fix: update lessons
This commit is contained in:
520
Les17-Externe-APIs/Les17-Lesstof.md
Normal file
520
Les17-Externe-APIs/Les17-Lesstof.md
Normal file
@@ -0,0 +1,520 @@
|
||||
# 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/
|
||||
Reference in New Issue
Block a user