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
- Beyond simple fetch
- Drie soorten authenticatie
- OAuth in Next.js
- Webhooks — wat en waarom
- Signature verification
- Stripe Checkout integratie
- Resend transactional email
- Rate limiting + retry patterns
- 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:
- Developers → Webhooks → Add endpoint
- URL:
https://your-app.com/api/webhook/stripe - Events:
checkout.session.completed,customer.subscription.deleted, etc. - 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
- GitHub —
X-Hub-Signature-256header, HMAC-SHA256 met shared secret - Resend —
svix-id,svix-timestamp,svix-signatureheaders - 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 betaling4000 0000 0000 0002— gedeclined4000 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
- Stripe Checkout: https://docs.stripe.com/checkout
- Stripe webhooks: https://docs.stripe.com/webhooks
- Stripe CLI: https://docs.stripe.com/stripe-cli
- Test cards: https://docs.stripe.com/testing
- Resend docs: https://resend.com/docs
- React Email: https://react.email/
- Auth.js: https://authjs.dev/
- Better-Auth: https://www.better-auth.com/
- Upstash Ratelimit: https://upstash.com/docs/oss/sdks/ts/ratelimit
- OAuth 2.0 spec: https://oauth.net/2/