fix: update lessons
This commit is contained in:
349
Les17-Externe-APIs/Les17-Huiswerk.md
Normal file
349
Les17-Externe-APIs/Les17-Huiswerk.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user