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

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.completed event 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 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