# 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) 1. Stripe account in test mode + product + price aangemaakt 2. Resend account + API key 3. Stripe CLI geïnstalleerd + `stripe login` gedaan 4. Demo-app `stripe-demo` werkend op laptop (backup) 5. ngrok / `stripe listen` getest 6. 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]` ```bash 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]*` ```typescript 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]*` ```bash 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]*` ```typescript 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]*` ```bash pnpm add resend ``` ```typescript 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 ", to: session.customer_email!, subject: "Welkom bij Premium!", html: `

Welkom!

Bedankt voor je aanmelding.

`, }); } ``` **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]*` ```bash 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." ```typescript async function retry(fn: () => Promise, attempts = 3): Promise { 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 |