8.1 KiB
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
# macOS
brew install stripe/stripe-cli/stripe
# of via https://docs.stripe.com/stripe-cli
stripe login
Lokaal webhook luisteren
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:
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.completedevent wordt herkend- Console toont event-data
Onderdeel B — Resend welkomstmail (verplicht)
Setup
pnpm add resend
Maak account op https://resend.com, kopieer API key.
.env.local:
RESEND_API_KEY=re_...
Email versturen in webhook
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:
pnpm add react-email @react-email/components
emails/WelcomeEmail.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:
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:
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:
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 listencommand- Endpoint code (link naar file)
- Signature verify (screenshot van falende verify als bewijs)
Sectie 3 — Voorbeeld successful flow
End-to-end:
- Pricing page klik
- Stripe Checkout (screenshot)
- Test-betaling (screenshot)
- Console output van webhook
- Email screenshot
- 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
- GitHub repo URL in Brightspace
APIS.mdin repo-root (5 secties)- Working Stripe + Resend demonstreerbaar
- 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