Files
novi-lessons/Les17-Externe-APIs/Les17-Huiswerk.md
2026-06-03 16:58:25 +02:00

350 lines
8.1 KiB
Markdown

# Les 17 — Huiswerk
## Webhook + Resend email + retry + APIS.md
**Deadline:** Voor Les 18 — Supabase Auth + RLS
**Inleveren:** GitHub repo + `APIS.md` in root
---
## Doel
Bouw je Stripe-demo uit naar een echte productie-flow: betaling triggert webhook → database update → welkomstmail. Plus retry-logic en eindopdracht-reflectie.
---
## Onderdeel A — Webhook handler met signature verify (verplicht)
### Stripe CLI installeren
```bash
# macOS
brew install stripe/stripe-cli/stripe
# of via https://docs.stripe.com/stripe-cli
stripe login
```
### Lokaal webhook luisteren
```bash
stripe listen --forward-to localhost:3000/api/webhook/stripe
```
Output: `Ready! Your webhook signing secret is whsec_xxx`. Kopieer naar `.env.local`:
```
STRIPE_WEBHOOK_SECRET=whsec_xxx
```
### Webhook endpoint
`app/api/webhook/stripe/route.ts`:
```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();
const sig = (await headers()).get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err: any) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
// Idempotency check
// const existing = await checkProcessedEvent(event.id);
// if (existing) return Response.json({ received: true });
console.log("Stripe event:", event.type);
if (event.type === "checkout.session.completed") {
const session = event.data.object;
console.log("Payment from:", session.customer_email);
// TODO: Insert in DB, send email
}
return Response.json({ received: true });
}
```
### Test
Met `stripe listen` aan + dev server aan, doe een test-betaling. Check console output van beide processen.
### Eisen
- [ ] Webhook endpoint werkt
- [ ] Signature verification actief (test door secret te wijzigen — moet falen)
- [ ] `checkout.session.completed` event wordt herkend
- [ ] Console toont event-data
---
## Onderdeel B — Resend welkomstmail (verplicht)
### Setup
```bash
pnpm add resend
```
Maak account op https://resend.com, kopieer API key.
`.env.local`:
```
RESEND_API_KEY=re_...
```
### Email versturen in webhook
```typescript
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY!);
// In webhook handler, na event.type check:
if (event.type === "checkout.session.completed") {
const session = event.data.object;
if (session.customer_email) {
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>`,
});
console.log("Email sent to:", session.customer_email);
}
}
```
### Optioneel: React Email
Voor mooiere templates:
```bash
pnpm add react-email @react-email/components
```
`emails/WelcomeEmail.tsx`:
```tsx
import { Html, Heading, Text, Button } from "@react-email/components";
export function WelcomeEmail({ email }: { email: string }) {
return (
<Html>
<Heading>Welkom!</Heading>
<Text>Je account ({email}) is geactiveerd.</Text>
<Button href="https://example.com/dashboard">Open dashboard</Button>
</Html>
);
}
```
In webhook:
```typescript
await resend.emails.send({
from: "...", to: session.customer_email!,
subject: "Welkom!",
react: WelcomeEmail({ email: session.customer_email! }),
});
```
### Eisen
- [ ] Email wordt verstuurd bij succesvolle betaling
- [ ] Email komt aan in inbox (check je eigen email als test)
- [ ] Resend dashboard toont email in logs
---
## Onderdeel C — Retry-logic (verplicht)
Maak een retry-helper en gebruik 'm voor minimaal één externe call.
`lib/retry.ts`:
```typescript
export 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);
console.log(`Retry ${i + 1}/${attempts} na ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error("unreachable");
}
```
Gebruik:
```typescript
const session = await retry(() =>
stripe.checkout.sessions.create({...})
);
```
### Demonstratie van een falende call
Voor je `APIS.md` (zie D): induceer expres een fail om retry te laten zien. Bv. wrong URL of fake timeout.
### Eisen
- [ ] `retry()` helper bestaat en werkt
- [ ] Wordt gebruikt op minimaal één externe call
- [ ] Log laat zien wanneer retries gebeuren
- [ ] In APIS.md: 1 voorbeeld van failed call + retry succes
---
## Onderdeel D — `APIS.md` (verplicht)
In repo-root.
### Sectie 1 — Welke APIs gebruikt jouw app
Tabel:
| API | Waarvoor | Auth | Cost |
|-----|----------|------|------|
| Stripe | Betalingen | secret key + webhook | 1.4% + €0.25 |
| Resend | Transactional email | api key | gratis tier 3k/maand |
### Sectie 2 — Webhook setup
Stappen die je nam:
- Stripe CLI installeren
- `stripe listen` command
- Endpoint code (link naar file)
- Signature verify (screenshot van falende verify als bewijs)
### Sectie 3 — Voorbeeld successful flow
End-to-end:
1. Pricing page klik
2. Stripe Checkout (screenshot)
3. Test-betaling (screenshot)
4. Console output van webhook
5. Email screenshot
6. Stripe dashboard transactie (screenshot)
### Sectie 4 — Failed call met retry
Voorbeeld waar retry redde:
```
Attempt 1: Failed (network timeout)
Attempt 2: Failed (network timeout)
Attempt 3: Success — session created
Total time: 4.2s
```
Welke fout je induceerde + waarom retry hielp.
### Sectie 5 — Eindopdracht-toepassing
Concrete plannen voor je eigen eindopdracht:
- Welke externe API ga je gebruiken?
- Waarom die?
- Webhook nodig? Welk event?
- Email-flows die je wilt?
- Wat ga je doen aan rate limiting?
Minstens 200 woorden, concreet.
### Vorm
- Max 800 woorden totaal
- Screenshots zijn waardevol
- Concrete code-references (links naar files)
---
## Bonus (optioneel)
### Bonus 1 — OAuth login met Auth.js
Voeg GitHub login toe met Auth.js. Documenteer setup-stappen.
### Bonus 2 — Idempotency in webhook
Sla event.id op in Supabase, skip als al verwerkt. Documenteer waarom belangrijk.
### Bonus 3 — Rate limiting
Voeg Upstash Ratelimit toe aan je `/api/checkout` endpoint. 5 calls per minuut per IP.
### Bonus 4 — Refund flow
Webhook ook luisteren naar `charge.refunded` event. Email sturen + DB update.
---
## Inleveren
1. **GitHub repo URL** in Brightspace
2. **`APIS.md`** in repo-root (5 secties)
3. **Working Stripe + Resend** demonstreerbaar
4. **Screenshots** in APIS.md van flow + Stripe + Resend dashboards
---
## Beoordeling
| Criterium | Punten |
|-----------|--------|
| A — Webhook werkt + signature verify | 3 |
| B — Resend email bij betaling | 2 |
| C — Retry helper + bewijs werking | 2 |
| D — APIS.md compleet (5 secties) | 3 |
| **Totaal** | **10** |
Voldoende = 6+. Bonus telt mee.
---
## Tijd-indicatie
| Onderdeel | Tijd |
|-----------|------|
| A — Webhook + verify | 30 min |
| B — Resend email | 25 min |
| C — Retry logic | 20 min |
| D — APIS.md | 45 min |
| **Totaal** | **~2 uur** |
---
## Veelvoorkomende valkuilen
| Probleem | Oplossing |
|----------|-----------|
| Signature verify faalt | Gebruik `req.text()` niet `req.json()` |
| Email komt niet aan | Check spam, check Resend logs |
| `stripe listen` toont 401 | `stripe login` opnieuw |
| Webhook secret klopt niet | Stripe CLI geeft een nieuwe secret per `stripe listen` sessie |
| Resend "domain not verified" | Gebruik `onboarding@resend.dev` voor demo |
| Headers reading errors | `await headers()` in Next 15+ |
---
## Tips
- **Test Stripe alleen in test mode** — productie key kan echte geld kosten
- **Stripe CLI is essentieel** voor lokaal testen
- **Test cards lijst:** https://docs.stripe.com/testing
- **Eindopdracht-tip:** kies één paid API (Stripe, Resend, Twilio) — laat zien dat je productie-flows snapt
- **Webhook idempotency** is een real-world musthave — bonus laat zien dat je productie-mindset hebt