15 KiB
Les 11 — Vercel AI SDK
Lesstof
Vak: AI-Assisted Development Opleiding: NOVI Hogeschool Utrecht Onderwerp: Vercel AI SDK — AI features bouwen, gekoppeld aan eigen dataset Demo: Polderfest 2027 — fictief festival, 500 records
Inhoudsopgave
- Wat is de Vercel AI SDK?
- Het modellen-landschap
- De vier kern-functies
- Project setup van A tot Z
- Seed script: dummy data in Supabase
- Chat-route + chat-UI implementeren
- Waarom data + AI samen krachtig zijn
- Best practices & valkuilen
- Wat komt hierna? Tool Calling teaser
- Bronnen
1. Wat is de Vercel AI SDK?
De Vercel AI SDK is een open-source TypeScript library waarmee je AI-features in je webapplicatie bouwt. Gemaakt door Vercel — de makers van Next.js — en daarom perfect geïntegreerd met React, Server Components en Server Actions.
Wat krijg je?
- Unified API — Zelfde code voor elk model (40+ providers)
- Streaming out-of-the-box — Geen WebSocket-setup nodig
- React hooks —
useChat,useCompletionvoor instant chat UI - Tool Calling — AI kan functies aanroepen die jij definieert (volgende les)
- Structured output — Type-safe data via
generateObject+ Zod - Multi-step agents — Via
maxSteps
Eerste indruk
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
const { text } = await generateText({
model: openai("gpt-4o-mini"),
prompt: "Vat de Polderfest 2027 line-up samen",
});
Wil je naar Anthropic? Eén regel veranderen:
import { anthropic } from "@ai-sdk/anthropic";
const { text } = await generateText({
model: anthropic("claude-sonnet-4"),
prompt: "Vat de Polderfest 2027 line-up samen",
});
Dat is de waarde.
2. Het modellen-landschap
| Provider | Model | Sterke punten | Prijs (in/out per 1M tokens) |
|---|---|---|---|
| OpenAI | gpt-4o-mini |
Snel, goedkoop, default | $0.15 / $0.60 |
| OpenAI | gpt-4o |
Multimodal (vision), krachtig | $2.50 / $10 |
| OpenAI | gpt-4.1 |
Reasoning, voor agents | $2 / $8 |
| Anthropic | claude-sonnet-4 |
Coding, lange context (200k) | $3 / $15 |
gemini-2.5-flash |
Multimodal, ultra goedkoop | $0.075 / $0.30 | |
| Groq | llama-3.3-70b |
Ultra-fast inference | $0.59 / $0.79 |
Vuistregel: start met gpt-4o-mini. Werkt het niet goed? Upgrade naar gpt-4o. Dan pas exotisch.
Wat kost onze Polderfest demo?
- Context = 500 bands → ~30.000 tokens per chat-request
- 1 vraag = ~$0.005 (een halve cent)
- 50 vragen = $0.25 (kwart euro)
Met gpt-4o (15× duurder) zou dezelfde demo ~$4 kosten. Daarom: start mini.
3. De vier kern-functies
generateText — Antwoord ophalen
Wacht tot het AI-antwoord compleet is, returnt dan een string.
const { text } = await generateText({
model: openai("gpt-4o-mini"),
prompt: "Geef 3 koffie-poll opties",
});
Wanneer: Korte antwoorden, server-only, niet-interactief.
streamText — Streaming antwoord
Streamt karakter voor karakter. Goed voor chat UI.
const result = streamText({
model: openai("gpt-4o-mini"),
messages,
});
return result.toDataStreamResponse();
Wanneer: Chat UI, lange antwoorden, "ChatGPT-gevoel". Vandaag gebruiken we dit.
useChat — React hook
Aan client-kant: complete chat UI in 10 regels.
"use client";
import { useChat } from "ai/react";
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<form onSubmit={handleSubmit}>
{messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
<input value={input} onChange={handleInputChange} />
</form>
);
}
Belangrijk: werkt alleen met streamText als API endpoint. Vandaag gebruiken we dit ook.
generateObject — Gestructureerde data
In plaats van een string krijg je type-safe data terug — gevalideerd met Zod.
const { object } = await generateObject({
model: openai("gpt-4o-mini"),
schema: z.object({
question: z.string(),
options: z.array(z.string()).length(4),
}),
prompt: "Maak een poll over koffie",
});
Wanneer: Database inserts, formulieren vullen, classificatie. Niet vandaag — komt terug in latere lessen.
4. Project setup van A tot Z
Dit is wat Tim in de les live deed. Voor je eigen project (lesopdracht/huiswerk): zelfde stappen.
Stap 1 — Next.js scaffolden
npx create-next-app@latest mijn-thema \
--typescript --tailwind --app --eslint --no-src-dir --turbopack
cd mijn-thema
Stap 2 — Supabase project aanmaken
- Ga naar https://supabase.com/dashboard
- New Project → kies naam
- Wacht ~2 min op deploy
- Settings → API → kopieer:
- Project URL
- anon public key
- service_role secret key
Stap 3 — Schema definiëren
Pas schema.sql aan voor jouw thema. Bv:
create table items (
id bigserial primary key,
name text not null,
category text,
rating int,
description text,
created_at timestamp default now()
);
Run in Supabase → SQL Editor.
Stap 4 — Env variables
.env.local:
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
OPENAI_API_KEY=sk-proj-...
Belangrijke regels:
NEXT_PUBLIC_*= client-leesbaarSUPABASE_SERVICE_ROLE_KEY= server-only, voor seed script (geenNEXT_PUBLIC_)OPENAI_API_KEY= server-only (geenNEXT_PUBLIC_)
Stap 5 — Packages installeren
npm install @supabase/supabase-js ai @ai-sdk/openai zod
npm install --save-dev tsx dotenv
Stap 6 — Supabase client
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!,
);
5. Seed script: dummy data in Supabase
Een seed-script is een TypeScript bestand dat éénmalig je tabel vult met dummy data. Geen handmatige inserts — procedureel gegenereerd.
Het Polderfest script (voorbeeld)
Het volledige seed-polderfest.ts zit als bijlage bij deze les. Kernidee:
import { createClient } from "@supabase/supabase-js";
import "dotenv/config";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const adjectives = ["Lost", "Velvet", "Iron", "Neon", "Silent", ...];
const nouns = ["Tigers", "Wolves", "Mirrors", "Clouds", ...];
const genres = ["Indie Rock", "Electronic", ...];
function generateBand(i: number) {
return {
name: `${pick(adjectives)} ${pick(nouns)}`,
genre: pick(genres),
// ...
};
}
async function seed() {
const bands = [];
for (let i = 0; i < 500; i++) bands.push(generateBand(i));
for (let i = 0; i < bands.length; i += 100) {
await supabase.from("bands").insert(bands.slice(i, i + 100));
}
}
seed();
Runnen
npx tsx scripts/seed-polderfest.ts
Waarom procedureel?
- Met 500 hard-coded records = 500 regels handmatige data → mind-numbing
- Met combinaties van 30 adjectives × 30 nouns = 900 unieke namen mogelijk
- Met seed-random = reproduceerbaar (zelfde data bij re-run)
Voor jouw eigen thema
Open seed-polderfest.ts, kopieer de structuur, en vervang de bouwstenen:
- Domein-specifieke arrays (in plaats van bands: restaurants, scripties, kunstwerken…)
- Domein-specifieke velden
- Domein-specifieke bio/beschrijving-fragmenten
Pro tip: vraag een AI om dit te doen! "Pas het Polderfest seed-script aan voor [thema]." OpenCode of Cursor doet dit in 30 seconden.
6. Chat-route + chat-UI implementeren
Dit is wat alleen nieuw is — Next.js + Supabase kennen jullie al.
De chat-route
app/api/chat/route.ts:
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
export async function POST(req: Request) {
const { messages } = await req.json();
// 1. Haal data op
const { data: bands } = await supabase.from("bands").select("*");
// 2. Format als context
const context = bands!
.map((b) =>
`- ${b.name} (${b.genre}, ${b.tier}, ${b.day} ${b.start_time})`,
)
.join("\n");
// 3. System prompt met context
const system = `Je bent een festival-assistent voor Polderfest 2027.
Hier zijn alle bands:
${context}
Beantwoord vragen op basis van bovenstaande data. Verzin niets.
Antwoord in het Nederlands.`;
// 4. Stream
const result = streamText({
model: openai("gpt-4o-mini"),
system,
messages,
});
return result.toDataStreamResponse();
}
De chat-pagina
app/chat/page.tsx:
"use client";
import { useChat } from "ai/react";
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, status } =
useChat();
return (
<main className="max-w-2xl mx-auto p-6 flex flex-col h-screen">
<h1 className="text-2xl font-bold mb-4">Polderfest 2027 — vraag de AI</h1>
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.map((m) => (
<div
key={m.id}
className={m.role === "user" ? "bg-blue-50 p-3 rounded-lg ml-12"
: "bg-gray-50 p-3 rounded-lg mr-12"}
>
<div className="font-medium text-sm text-gray-500 mb-1">
{m.role === "user" ? "Jij" : "Festival AI"}
</div>
<div className="whitespace-pre-wrap">{m.content}</div>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Stel een vraag over de line-up..."
className="flex-1 p-3 border rounded-lg"
/>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg"
>
Stuur
</button>
</form>
</main>
);
}
Voorbeeld-vragen die we live stelden
| Vraag | Wat AI doet |
|---|---|
| "Welke bands spelen zaterdag op de Beach Stage?" | Filtert door context |
| "3 headliners met meeste populariteit" | Sorteert + select top |
| "Hoeveel jazz fusion acts totaal?" | Telt |
| "Vat de electronic-scene samen" | Samenvatting — alleen AI kan dit |
| "Wie was hoofdact van Polderfest 2025?" | Eerlijk: "weet ik niet" — perfect |
7. Waarom data + AI samen krachtig zijn
Data alleen
- SQL queries: filter, sort, select
- Geen interpretatie, geen taal, geen samenvatting
- Gebruiker moet zelf SQL kunnen
AI alleen
- Kennis is generiek (training data)
- Verzint vaak (hallucinatie)
- Geen toegang tot live data of private data
Data + AI
- AI filtert via reasoning op tekst-context
- Antwoorden in natuurlijke taal
- Samenvattingen en interpretatie
- Domein-kennis = jouw data, AI redeneert erover
"Een LLM zonder jouw data is een gewone chatbot. Een LLM mét jouw data is een product."
8. Best practices & valkuilen
Doen
- Begin met
gpt-4o-mini— upgrade pas als nodig - System prompt is essentieel — "gebruik alleen onze data, verzin niets"
- Stream alles —
streamTextvoelt 5× sneller dangenerateText - AI-calls altijd server-side — keys blijven veilig
- Loading state — AI duurt 1-5 sec, zonder feedback voelt het stuk
- Foutafhandeling —
try/catchrond elke AI-call
Niet doen
- Geen
NEXT_PUBLIC_OPENAI_API_KEY— wordt zichtbaar in client - Niet de output blind vertrouwen — AI hallucineert
- Niet alle data altijd meesturen — werkt voor 500 records, niet voor 50.000 (volgende les)
- Niet
gpt-4oals default — 15× duurder dan mini, vaak onnodig
Veelvoorkomende fouten
| Fout | Oorzaak | Oplossing |
|---|---|---|
OPENAI_API_KEY is not defined |
.env.local niet geladen |
Dev server herstarten |
Cannot find module 'ai' |
npm install vergeten | npm i ai @ai-sdk/openai |
Seed: permission denied |
Anon key i.p.v. service role | Gebruik SUPABASE_SERVICE_ROLE_KEY |
| AI antwoordt in Engels | Niet expliciet om NL gevraagd | System prompt aanpassen |
| AI verzint feiten | System prompt te zwak | Voeg toe: "verzin niets, gebruik alleen onze data" |
| Chat laadt niet | useChat zonder streamText API |
Endpoint moet result.toDataStreamResponse() returnen |
9. Wat komt hierna?
Het schaalprobleem
Vandaag sturen we alle 500 bands mee als context bij elke request. Dat is ~30k tokens. Werkt prima voor 500. Werkt niet voor:
- 5.000 records → te duur, te traag
- 50.000 records → past niet in context window
- Real-time data → context wordt steeds opnieuw gebouwd
Volgende les: Tool Calling (Les 12)
In plaats van alle data te sturen, geef je de AI tools (functies). De AI besluit zelf welke te gebruiken:
const { text } = await generateText({
model: openai("gpt-4o-mini"),
messages,
tools: {
searchBands: tool({
description: "Zoek bands op dag, stage, of genre",
parameters: z.object({
day: z.string().optional(),
stage: z.string().optional(),
genre: z.string().optional(),
}),
execute: async ({ day, stage, genre }) => {
// Supabase query
const { data } = await supabase.from("bands").select("*")
.eq("day", day || undefined)
.eq("stage", stage || undefined);
return data;
},
}),
},
maxSteps: 5,
});
Workflow:
- User: "Welke bands op vrijdag?"
- AI: "Ik roep
searchBands({ day: 'Vrijdag' })aan" - Supabase: 60 bands terug
- AI: "Op vrijdag spelen 60 bands. De headliners zijn..."
Schaalbaar. Slim. Multi-step (combineer meerdere tools).
Daarna in deze leerlijn
- Les 13: Agents +
maxSteps(autonome multi-step taken) - Les 14: RAG + embeddings (semantic search op heel grote datasets)
- Les 15-16: Testing + Deployment + Performance
- Les 17-18: Eindopdracht-werkdagen + Pitch
10. Bronnen
Vercel AI SDK
- Hoofdpagina: https://ai-sdk.dev/docs/introduction
- Voorbeelden: https://ai-sdk.dev/examples
streamText: https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-textuseChat: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat- Tool Calling (volgende les): https://ai-sdk.dev/docs/foundations/tools
Supabase
- JS Client: https://supabase.com/docs/reference/javascript
- Row Level Security: https://supabase.com/docs/guides/auth/row-level-security
- Server-side usage: https://supabase.com/docs/guides/auth/server-side
Inspiratie
- v0.dev — Generative UI in actie
- chat.vercel.ai — Officiële demo van AI SDK
- Vercel templates met AI: https://vercel.com/templates?type=ai
Tokens & kosten
- OpenAI pricing: https://openai.com/api/pricing
- Tokenizer: https://platform.openai.com/tokenizer
- Usage dashboard: https://platform.openai.com/usage