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

13 KiB

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
  2. Drie soorten authenticatie
  3. OAuth in Next.js
  4. Webhooks — wat en waarom
  5. Signature verification
  6. Stripe Checkout integratie
  7. Resend transactional email
  8. Rate limiting + retry patterns
  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.

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:

pnpm add next-auth@beta

auth.ts:

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:

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:

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

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:

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

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

  • GitHubX-Hub-Signature-256 header, HMAC-SHA256 met shared secret
  • Resendsvix-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)?

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

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:

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

"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

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:

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

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:

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.

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:

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