fix: update lessons

This commit is contained in:
2026-06-03 16:58:25 +02:00
parent eb1ba2e28d
commit a852aa0d52
83 changed files with 16830 additions and 0 deletions

View File

@@ -0,0 +1,520 @@
# 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";
<form action={async () => { "use server"; await signIn("github"); }}>
<button type="submit">Login met GitHub</button>
</form>
```
Klaar. Auth.js regelt de hele flow.
### Sessie ophalen
```typescript
import { auth } from "@/auth";
const session = await auth();
if (!session?.user) return <p>Niet ingelogd</p>;
return <p>Hallo {session.user.name}</p>;
```
### 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 (
<Html>
<Heading>Welkom {name}!</Heading>
<Text>Bedankt voor je aanmelding bij Premium.</Text>
<Button href="https://app.example.com/dashboard">
Ga naar dashboard
</Button>
</Html>
);
}
```
### Verzenden
```typescript
import { Resend } from "resend";
import { WelcomeEmail } from "@/emails/WelcomeEmail";
const resend = new Resend(process.env.RESEND_API_KEY!);
await resend.emails.send({
from: "Acme <onboarding@resend.dev>",
to: "user@example.com",
subject: "Welkom bij Premium!",
react: WelcomeEmail({ name: "Tim" }),
});
```
### Combineer met webhook
In Stripe webhook handler:
```typescript
if (event.type === "checkout.session.completed") {
const session = event.data.object;
await resend.emails.send({
from: "noreply@example.com",
to: session.customer_email!,
subject: "Welkom bij Premium!",
react: WelcomeEmail({ name: session.customer_details!.name! }),
});
}
```
User betaalt → Stripe webhook → Resend verstuurt welkomstmail. Volledig automatisch.
---
## 8. Rate limiting + retry patterns
### Exponential backoff retry
Wanneer externe API tijdelijk faalt (rate limit, server-side error, network glitch), gewoon retryen werkt vaak. Met exponential backoff: wachtinterval verdubbelt elke retry.
```typescript
async function retry<T>(
fn: () => Promise<T>,
attempts = 3,
baseDelayMs = 1000
): Promise<T> {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts - 1) throw err;
const delay = baseDelayMs * Math.pow(2, i); // 1s, 2s, 4s
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error("unreachable");
}
// Gebruik:
const data = await retry(() =>
fetch("https://api.flakyservice.com/").then(r => {
if (!r.ok) throw new Error("API failed");
return r.json();
})
);
```
**Wanneer NIET retryen:**
- 4xx errors (jouw fout — geen retry helpt)
- POST endpoints zonder idempotency
- Vereist user-input correctie
**Wanneer WEL:**
- 5xx errors (server-side fault)
- 429 Too Many Requests (rate limited)
- Network timeouts
### Rate limit eigen endpoints
Bescherm je eigen API tegen abuse / overuse:
```typescript
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10s"), // 10 calls per 10 sec
});
export async function POST(req: Request) {
const ip = req.headers.get("x-forwarded-for") ?? "unknown";
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return new Response("Too many requests", {
status: 429,
headers: { "X-RateLimit-Limit": `${limit}`, "X-RateLimit-Remaining": `${remaining}` },
});
}
// normale handler...
}
```
Upstash heeft een gratis tier (10k requests/dag). Voor productie genoeg voor middelgrote apps.
### Fail fast vs retry stilletjes
User-facing endpoints: fail fast met duidelijke error. Background jobs: retry achteraf met queue.
---
## 9. Productie checklist
Voor je live gaat met externe API integratie:
### API keys
- ✓ Test-keys voor preview, prod-keys voor productie
- ✓ Vercel env scoping per environment
- ✓ Rotate keys eens per kwartaal
- ✓ Geen keys in client-bundle (geen `NEXT_PUBLIC_`)
### Webhooks
- ✓ Signature verification ALTIJD
- ✓ Idempotency check (event.id in DB)
- ✓ Log alle webhook payloads
- ✓ Return 200 snel, doe verwerking async indien nodig
- ✓ Handle alle relevante event types
### Errors
- ✓ Exponential backoff voor transient fails
- ✓ Rate limit eigen endpoints
- ✓ Monitor met Sentry / LogRocket / Vercel Analytics
- ✓ Alerting bij webhook failures
### Security
- ✓ HTTPS only (Vercel default)
- ✓ Webhook endpoints accepteren alleen verified signatures
- ✓ User-input validatie met Zod
- ✓ Geen secrets in logs
### Cost monitoring
- ✓ Stripe Dashboard — usage + revenue
- ✓ Resend Dashboard — email count + bounces
- ✓ Set up budget alerts in services die dat ondersteunen
- ✓ Cache externe responses waar mogelijk
---
## Bronnen
- **Stripe Checkout:** https://docs.stripe.com/checkout
- **Stripe webhooks:** https://docs.stripe.com/webhooks
- **Stripe CLI:** https://docs.stripe.com/stripe-cli
- **Test cards:** https://docs.stripe.com/testing
- **Resend docs:** https://resend.com/docs
- **React Email:** https://react.email/
- **Auth.js:** https://authjs.dev/
- **Better-Auth:** https://www.better-auth.com/
- **Upstash Ratelimit:** https://upstash.com/docs/oss/sdks/ts/ratelimit
- **OAuth 2.0 spec:** https://oauth.net/2/