# Les 17 — Lesstof ## Externe APIs in diepte — OAuth, webhooks, paid APIs **Vak:** AI-Assisted Development **Vorige les:** Les 16 — MCP **Volgende les:** Les 18 — Supabase Auth + RLS --- ## Inhoud 1. [Beyond simple fetch](#1-beyond-simple-fetch) 2. [Drie soorten authenticatie](#2-drie-soorten-authenticatie) 3. [OAuth in Next.js](#3-oauth-in-nextjs) 4. [Webhooks — wat en waarom](#4-webhooks--wat-en-waarom) 5. [Signature verification](#5-signature-verification) 6. [Stripe Checkout integratie](#6-stripe-checkout-integratie) 7. [Resend transactional email](#7-resend-transactional-email) 8. [Rate limiting + retry patterns](#8-rate-limiting--retry-patterns) 9. [Productie checklist](#9-productie-checklist) --- ## 1. Beyond simple fetch In Les 15 deden we simpele externe APIs: PokéAPI, geen key, GET request, JSON terug. Voor productie-apps heb je vaak meer nodig. Wat onderscheidt een 'productie' API-integratie van een simpele fetch: - **Authentication** — key in header, OAuth token, of beide - **Webhooks** — externe service belt JOUW app terug - **Idempotency** — wat als request twee keer binnenkomt? - **Rate limiting** — niet meer dan X calls per minuut - **Error handling** — wat als de API down is? Retry? - **Cost monitoring** — paid APIs lopen op - **Security** — secrets, signatures, validation Vandaag focus op de eerste twee — daar komen de meeste productie-bugs vandaan. --- ## 2. Drie soorten authenticatie ### API key in header Simpelst. Key in env var, stuur mee als `Authorization` header. ```typescript const res = await fetch("https://api.openai.com/v1/...", { headers: { "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json", }, }); ``` Voorbeelden: OpenAI, Anthropic, Tavily, Resend, Stripe (voor server-side calls). ### OAuth flow Voor user-authenticatie. Gebruiker logt in via Google/GitHub/etc., jouw server krijgt access token. Zie sectie 3. Voorbeelden: Google login, GitHub login, Spotify "login with", Slack apps. ### Webhook signatures Geen auth voor calls *naar* die service, maar wel verificatie van calls *van* die service. Zie sectie 5. Voorbeelden: Stripe webhooks, GitHub webhooks, Resend webhooks. --- ## 3. OAuth in Next.js ### De flow ``` 1. User klikt "Login met GitHub" 2. Redirect naar github.com/login/oauth/authorize?client_id=...&redirect_uri=... 3. User logt in, geeft toestemming 4. GitHub redirect terug naar JOUW redirect_uri met ?code=xyz 5. JOUW server: POST naar github.com/login/oauth/access_token met code 6. Krijgt access_token terug 7. Optioneel: haal user-info op met token (GET api.github.com/user) 8. JOUW server zet sessie cookie ``` ### Auth.js (NextAuth) Meest populaire library. Installeren: ```bash pnpm add next-auth@beta ``` `auth.ts`: ```typescript import NextAuth from "next-auth"; import GitHub from "next-auth/providers/github"; export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [GitHub], }); ``` `app/api/auth/[...nextauth]/route.ts`: ```typescript import { handlers } from "@/auth"; export const { GET, POST } = handlers; ``` `.env.local`: ``` AUTH_SECRET=... # pnpm dlx auth secret AUTH_GITHUB_ID=... AUTH_GITHUB_SECRET=... ``` Login button: ```tsx import { signIn } from "@/auth";
``` Klaar. Auth.js regelt de hele flow. ### Sessie ophalen ```typescript import { auth } from "@/auth"; const session = await auth(); if (!session?.user) returnNiet ingelogd
; returnHallo {session.user.name}
; ``` ### Wanneer NIET Auth.js - Wil je alleen Supabase-data + auth → gebruik Supabase Auth (Les 18) - Hele klein project, één provider → custom OAuth in 50 regels werkt prima - Enterprise-feel met SSO → Clerk of Auth0 (paid) --- ## 4. Webhooks — wat en waarom ### Het probleem Stel je hebt Stripe Checkout. User klikt "Betaal", redirect naar Stripe, betaalt, redirect terug naar jouw success page. Maar wat als: - User sluit tab voor success page laadt? - Betaling gaat eerst pending, pas later confirmed? - Refund — gebeurt dagen later? **Polling Stripe-API elke seconde is inefficiënt.** Webhooks zijn de oplossing. ### Wat zijn webhooks Webhook = HTTP endpoint dat door externe service wordt aangeroepen wanneer er iets gebeurt. ``` Stripe processes payment ↓ Stripe sends POST to your-app.com/api/webhook/stripe with JSON body: { type: "checkout.session.completed", data: {...} } ↓ Your server: update database, send email, etc. ``` ### Hoe registreer je een webhook **Stripe Dashboard:** 1. Developers → Webhooks → Add endpoint 2. URL: `https://your-app.com/api/webhook/stripe` 3. Events: `checkout.session.completed`, `customer.subscription.deleted`, etc. 4. Krijg webhook signing secret terug (`whsec_...`) **Lokaal testen** met Stripe CLI: ```bash stripe listen --forward-to localhost:3000/api/webhook/stripe ``` CLI geeft een webhook secret terug die je tijdelijk in `.env.local` zet. --- ## 5. Signature verification **Belangrijk:** zonder verificatie 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. ### Stripe pattern ```typescript import { headers } from "next/headers"; import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(req: Request) { const body = await req.text(); // RAW body, geen 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(`Webhook Error: ${err.message}`, { status: 400 }); } // event is verified, veilig om te gebruiken switch (event.type) { case "checkout.session.completed": // handle... break; } return Response.json({ received: true }); } ``` **Cruciaal:** `req.text()`, niet `req.json()`. Stripe berekent signature over de raw bytes. ### Andere providers - **GitHub** — `X-Hub-Signature-256` header, HMAC-SHA256 met shared secret - **Resend** — `svix-id`, `svix-timestamp`, `svix-signature` headers - **Eigen webhooks** — gebruik dezelfde pattern: shared secret + HMAC ### Idempotency Wat als Stripe een event 2x stuurt (door netwerk-issues)? ```typescript const { data: existing } = await supabase .from("processed_events") .select() .eq("stripe_event_id", event.id) .single(); if (existing) return Response.json({ received: true }); // skip // handle event... await supabase.from("processed_events").insert({ stripe_event_id: event.id }); ``` Sla `event.id` op, check voor handle. Voorkomt dubbele betalingen, dubbele emails, dubbele credits. --- ## 6. Stripe Checkout integratie ### Setup ```bash pnpm add stripe ``` `.env.local`: ``` STRIPE_SECRET_KEY=sk_test_... STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_WEBHOOK_SECRET=whsec_... ``` ### Maak Checkout session `app/api/checkout/route.ts`: ```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", // of "payment" voor eenmalig 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}/pricing`, }); return Response.json({ url: session.url }); } ``` ### Client redirect ```tsx "use client"; async function subscribe() { const res = await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ priceId: "price_xxx", email: "test@test.com" }), }); const { url } = await res.json(); window.location.href = url; // redirect naar Stripe } ``` ### Test cards Stripe test mode (geen echt geld): - `4242 4242 4242 4242` — succesvolle betaling - `4000 0000 0000 0002` — gedeclined - `4000 0025 0000 3155` — 3D Secure required Elke datum in toekomst voor expiry, willekeurige 3-cijferige CVC. --- ## 7. Resend transactional email ### Setup ```bash pnpm add resend react-email ``` `.env.local`: ``` RESEND_API_KEY=re_... ``` Voor demo: gebruik `onboarding@resend.dev` als from-address (geen eigen domain nodig). Voor productie: registreer een eigen domain in Resend Dashboard. ### Email als React component `emails/WelcomeEmail.tsx`: ```tsx import { Html, Heading, Text, Button } from "@react-email/components"; export function WelcomeEmail({ name }: { name: string }) { return (