11 KiB
Les 17 — Externe APIs in diepte
Docenttekst (Klas A — 3 uur, fysiek, demo-driven)
Les: 17 van 18
Onderwerp: OAuth, webhooks, Stripe, Resend, retry patterns
Duur: 180 minuten
Demo-app: stripe-demo — subscription-flow met webhook en Resend mail
VÓÓR DE LES (45 min)
- Stripe account in test mode + product + price aangemaakt
- Resend account + API key
- Stripe CLI geïnstalleerd +
stripe logingedaan - Demo-app
stripe-demowerkend op laptop (backup) - ngrok /
stripe listengetest - Browser tabs: Stripe Dashboard, Resend Dashboard, docs.stripe.com, resend.com/docs
HET SCRIPT
BLOK 1 — Welkom + Recap (15 min)
[SLIDE 1] [SCHERM: slides]
Vertel: "Welkom bij les 17. We gaan vandaag externe APIs uitkluizen op productie-niveau. OAuth, webhooks, paid services. De real-world stuff die je vorige les met PokéAPI niet zag."
[SLIDE 2]
Vertel: "Tot nu toe deden we externe APIs eenvoudig. PokéAPI in les 15 — geen key, GET request, klaar. Maar in productie kom je dingen tegen die je nog niet hebt gezien. Login met Google. Betalingen. Email versturen. Wat als de API een minuut down is?
Vandaag bouwen we een mini subscription-app met Stripe en Resend. Geen echt geld — Stripe heeft een test mode. Maar de patronen zijn productie-klaar."
[SLIDE 3]
Vertel: "Drie uur. 50 min theorie, vier demo's, lesopdracht en huiswerk."
BLOK 2 — Theorie (40 min)
[SLIDE 4 — Drie soorten API integratie]
Vertel: "Externe APIs komen in drie smaken qua auth. Simple fetch — geen key, alles publiek. PokéAPI is daar het schoolvoorbeeld van.
Key in header — de meeste paid APIs. OpenAI, Anthropic, Tavily. Eén string in je env vars, mee in elke request.
OAuth flow — voor user-authenticatie. Google login, GitHub login. Complexer.
En een vierde dimensie: webhooks. Geen auth voor calls naar een service, maar wel signature verificatie van calls van een service. Daar focussen we vandaag op."
[SLIDE 5 — OAuth basics]
Vertel: "OAuth in zeven stappen. Klikt 'login met GitHub'. Redirect naar github.com. User logt in, geeft toestemming. GitHub redirect terug met een code. Jouw server exchanged die code voor een access token. Met dat token haal je user-info op. Cookie/sessie zetten. Klaar.
In Next.js — Auth.js is de standaard. Drie regels code, één env var, werkt. Niet vandaag bouwen — komt in huiswerk als bonus, ook in les 18 met Supabase Auth."
[SLIDE 6 — Webhooks]
Vertel: "Webhooks. Het tegenovergestelde van een API-call. Niet jij belt de service, de service belt jou.
Stel je hebt Stripe Checkout. User betaalt. Stripe verwerkt. Mogelijk gaat het mis — kaart geweigerd, fraud-check. Mogelijk duurt het seconden, of minuten bij 3D Secure.
Polling Stripe API elke seconde — duur, traag, lelijk. Webhook — Stripe stuurt een POST naar JOUW endpoint zodra er iets gebeurt. Jij update database, stuurt email, etc.
Cruciaal: signature verification. Zonder dat kan iedereen je webhook URL aanroepen met fake data. Een fraudeur stuurt fake 'betaling completed' event — jouw server geeft premium toegang aan iemand die niets heeft betaald. Altijd verifiëren."
BLOK 3 — DEMO 1: Stripe Checkout (25 min)
[SLIDE 7] + [SLIDE 8] [SCHERM: terminal + editor + stripe dashboard]
cd ~/novi/novi-lessons/Les17-Externe-APIs
pnpm create next-app@latest stripe-demo --typescript --tailwind --app
cd stripe-demo
pnpm add stripe
*[Stripe Dashboard]* [SCHERM: browser]
Vertel: "Eerst Stripe. Test mode aan, rechtsboven. API keys ophalen. Belangrijk: sk_test_ is je secret key — server-only. pk_test_ is publishable, mag in client."
*[Maak product + price in Stripe Dashboard]*
*[.env.local aanvullen]*
*[app/api/checkout/route.ts — code typen]*
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const { priceId, email } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
customer_email: email,
success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/`,
});
return Response.json({ url: session.url });
}
Vertel: "Mode subscription voor recurring. Mode payment voor eenmalig. Line items met price ID. Success en cancel URLs. Stripe geeft een URL terug — daar redirect je naartoe."
*[Pricing page + button + test]*
*[Open browser, klik subscribe, Stripe Checkout opent]*
Vertel: "Test card. 4242 4242 4242 4242. Elke datum future, willekeurige CVC. Klik subscribe."
*[Betaling slaagt, redirect naar /success]*
Vertel: "Werkt. Check Stripe Dashboard, payments tab — transactie staat erin. Geen echt geld. Test mode."
BLOK 4 — DEMO 2: Webhook (25 min)
[SLIDE 9] [SCHERM: terminal + editor]
Vertel: "Maar — we hebben een probleem. Wat als user de tab sluit voor de success page laadt? Dan weten we niet of er betaald is. Webhook lost dit op."
*[Stripe CLI lokaal]*
stripe listen --forward-to localhost:3000/api/webhook/stripe
Vertel: "Stripe CLI doet een trick — registreert tijdelijk een webhook voor jouw lokale poort. Output geeft je een signing secret. Kopieer naar .env.local."
*[Webhook handler schrijven]*
import { headers } from "next/headers";
export async function POST(req: Request) {
const body = await req.text(); // RAW body, niet json()
const sig = (await headers()).get("stripe-signature")!;
let event;
try {
event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return new Response("Bad signature", { status: 400 });
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
console.log("Payment:", session.customer_email);
}
return Response.json({ received: true });
}
Vertel: "Drie cruciale dingen. Eén — req.text(), niet req.json(). Stripe berekent signature over raw bytes. Twee — try-catch om constructEvent. Verkeerde signature = 400 terug. Drie — check event.type voor wat je wilt afhandelen."
*[Doe test-betaling]*
Vertel: "Webhook event komt binnen. Stripe CLI toont 'Event received'. Mijn server logt 'Payment: test@example.com'. Werkt."
💬 Vraag: 'Wat als ik de URL niet weet? Of geen Stripe CLI heb?'
Antwoord: "Voor lokale dev: Stripe CLI is verreweg de simpelste. Anders kun je ngrok gebruiken — geeft je een publieke URL voor je localhost. Of localhost.run. Productie: gewoon je Vercel URL, geen tooling nodig."
BLOK 5 — Pauze (15 min)
[SLIDE 10]
BLOK 6 — DEMO 3: Resend (25 min)
[SLIDE 11] [SCHERM: editor + resend dashboard]
Vertel: "Mail versturen. Resend is de moderne keuze — opgericht door ex-Vercel mensen, mooie DX, gratis tier 3000 emails per maand."
*[Maak Resend account, kopieer API key]*
pnpm add resend
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY!);
// In webhook handler
if (event.type === "checkout.session.completed") {
const session = event.data.object;
await resend.emails.send({
from: "Premium <onboarding@resend.dev>",
to: session.customer_email!,
subject: "Welkom bij Premium!",
html: `<h1>Welkom!</h1><p>Bedankt voor je aanmelding.</p>`,
});
}
Vertel: "Voor demo: onboarding@resend.dev als from. Geen eigen domain nodig. Voor productie: registreer een domain in Resend Dashboard, verify DNS, gebruik dat als from."
*[Test: doe een betaling, mail komt aan]*
Vertel: "Mail aangekomen. Bekijk in Resend Dashboard — bevat ook delivery status, opens, clicks."
*[Optioneel — react-email demo]*
pnpm add react-email @react-email/components
*[Maak emails/WelcomeEmail.tsx]*
Vertel: "Voor mooiere mails — React-email componenten. Schrijf JSX, Resend rendert naar HTML met inline styles voor maximale email-client compatibility."
BLOK 7 — DEMO 4: Retry + Rate limit (15 min)
[SLIDE 12] [SCHERM: editor]
Vertel: "Laatste pattern. Externe APIs falen. Soms transient — netwerk-issue, server hiccup. Soms permanent — wrong key, validation error. Retry-logic voor transient."
async function retry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, 2 ** i * 1000));
}
}
throw new Error("unreachable");
}
Vertel: "Exponential backoff. Eerste retry na 1 seconde. Tweede na 2. Derde na 4. Voorkomt thundering herd op een herstellende service. Wrap je externe calls hierin."
Vertel: "Rate limit eigen endpoints — Upstash Ratelimit, gratis tier. Hou DDOS en abuse buiten. Vooral relevant voor je AI-endpoints — een gehackte user kan veel OpenAI-credits verbranden."
BLOK 8 — Productie checklist (10 min)
[SLIDE 13]
Vertel: "Voor je live gaat. Vink af.
API keys per environment — Vercel scoping uit les 15. Webhooks ALTIJD signature verify. Idempotency check — zelfde event 2x is OK. Retry-logic voor transient. Rate limit eigen endpoints. Monitor met Sentry of Vercel Analytics.
Plus security: keys NOOIT in client, geen NEXT_PUBLIC_ voor secrets, HTTPS only, user-input validatie met Zod.
Tien minuten voor er aan denken, uren bug-fixen later. Voor je eindopdracht: check deze lijst."
BLOK 9 — Lesopdracht + Huiswerk (10 min)
[SLIDE 14]
Vertel: "Lesopdracht — half uur. Stripe account, product, Next app, Checkout endpoint, test-betaling. Geen webhook nog.
Huiswerk — webhook met verify, Resend mail in webhook, retry-logic, en APIS.md. Vooral die laatste — reflectie op welke externe API je in je eindopdracht gaat gebruiken. Wat je geleerd hebt direct toepassen.
Beoordeling tien punten, voldoende zes. Bonus: OAuth via Auth.js, idempotency, rate limiting, refund-flow."
BLOK 10 — Afsluiting (5 min)
[SLIDE 15]
Vertel: "Wat hebben we gezien. Drie soorten API auth. OAuth basics. Webhooks met signature verify. Stripe Checkout. Resend email. Retry en rate limit.
Volgende les — Les 18 — is de laatste. Supabase Auth en RLS. Multi-user apps met login. Per-user data isolation. We sluiten af met een combo van alles.
Vragen?"
Veelvoorkomende fouten
| Fout | Oplossing |
|---|---|
| Webhook signature fails | req.text() ipv req.json() |
headers is not a function |
await headers() in Next 15+ |
| Stripe CLI 401 | stripe login opnieuw |
| Mail komt niet aan | Check spam + Resend dashboard logs |
STRIPE_WEBHOOK_SECRET wrong |
Stripe CLI geeft nieuwe secret per stripe listen |
| Resend domain error | Gebruik onboarding@resend.dev voor demo |
| Test card geweigerd | Test mode aan? Rechtsboven Stripe dashboard |