521 lines
13 KiB
Markdown
521 lines
13 KiB
Markdown
# 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](#1-beyond-simple-fetch)
|
|
2. [Drie soorten authenticatie](#2-drie-soorten-authenticatie)
|
|
3. [OAuth in Next.js](#3-oauth-in-nextjs)
|
|
4. [Webhooks — wat en waarom](#4-webhooks--wat-en-waarom)
|
|
5. [Signature verification](#5-signature-verification)
|
|
6. [Stripe Checkout integratie](#6-stripe-checkout-integratie)
|
|
7. [Resend transactional email](#7-resend-transactional-email)
|
|
8. [Rate limiting + retry patterns](#8-rate-limiting--retry-patterns)
|
|
9. [Productie checklist](#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.
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```bash
|
|
pnpm add next-auth@beta
|
|
```
|
|
|
|
`auth.ts`:
|
|
```typescript
|
|
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`:
|
|
```typescript
|
|
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:
|
|
```tsx
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
```bash
|
|
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
|
|
|
|
```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(); // 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-256` header, HMAC-SHA256 met shared secret
|
|
- **Resend** — `svix-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)?
|
|
|
|
```typescript
|
|
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
|
|
|
|
```bash
|
|
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`:
|
|
```typescript
|
|
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
|
|
|
|
```tsx
|
|
"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
|
|
|
|
```bash
|
|
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`:
|
|
```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
|
|
|
|
```typescript
|
|
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:
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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/
|