Loading...
Loading...
Patterns for reliable external service integration: env validation, health checks, error handling, observability. Invoke when integrating Stripe, Clerk, Sendgrid, or any external API.
npx skill4agent add phrazzld/claude-config external-integration-patternswebhookapi/services/External services fail. Your integration must be observable, recoverable, and fail loudly.
// At module load, NOT inside a function
const REQUIRED = ['SERVICE_API_KEY', 'SERVICE_WEBHOOK_SECRET'];
for (const key of REQUIRED) {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required env var: ${key}`);
}
if (value !== value.trim()) {
throw new Error(`${key} has trailing whitespace — check dashboard for invisible characters`);
}
}
// Now safe to use
export const apiKey = process.env.SERVICE_API_KEY!;// /api/health/route.ts or /api/health/[service]/route.ts
export async function GET() {
const checks: Record<string, { ok: boolean; latency?: number; error?: string }> = {};
// Check Stripe
try {
const start = Date.now();
await stripe.balance.retrieve();
checks.stripe = { ok: true, latency: Date.now() - start };
} catch (e) {
checks.stripe = { ok: false, error: e.message };
}
// Check database
try {
const start = Date.now();
await db.query.users.findFirst();
checks.database = { ok: true, latency: Date.now() - start };
} catch (e) {
checks.database = { ok: false, error: e.message };
}
const healthy = Object.values(checks).every(c => c.ok);
return Response.json({
status: healthy ? 'ok' : 'degraded',
checks,
timestamp: new Date().toISOString()
}, { status: healthy ? 200 : 503 });
}catch (error) {
// Structured JSON for log aggregation
console.error(JSON.stringify({
level: 'error',
service: 'stripe',
operation: 'createCheckout',
userId: user.id,
input: { priceId, mode }, // Safe subset of input
error: error.message,
code: error.code || 'unknown',
timestamp: new Date().toISOString()
}));
throw error;
}serviceoperationuserIderrortimestampexport async function handleWebhook(req: Request) {
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
// 1. Verify signature FIRST (before any processing)
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (e) {
console.error(JSON.stringify({
level: 'error',
source: 'webhook',
service: 'stripe',
error: 'Signature verification failed',
message: e.message
}));
return new Response('Invalid signature', { status: 400 });
}
// 2. Log event received BEFORE processing
console.log(JSON.stringify({
level: 'info',
source: 'webhook',
service: 'stripe',
eventType: event.type,
eventId: event.id,
timestamp: new Date().toISOString()
}));
// 3. Store event for reconciliation (optional but recommended)
await db.insert(webhookEvents).values({
provider: 'stripe',
eventId: event.id,
eventType: event.type,
payload: event,
processedAt: null
});
// 4. Return 200 quickly, process async if slow
// (Stripe retries if response takes too long)
await processEvent(event);
return new Response('OK', { status: 200 });
}// Run hourly or daily
export async function reconcileSubscriptions() {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// Fetch active subscriptions modified in last 24h
const subs = await stripe.subscriptions.list({
status: 'active',
created: { gte: Math.floor(Date.now() / 1000) - 86400 }
});
for (const sub of subs.data) {
// Update local state to match Stripe
await db.update(subscriptions)
.set({ status: sub.status, currentPeriodEnd: sub.current_period_end })
.where(eq(subscriptions.stripeId, sub.id));
}
console.log(JSON.stringify({
level: 'info',
operation: 'reconcileSubscriptions',
synced: subs.data.length,
timestamp: new Date().toISOString()
}));
}// /checkout/success/page.tsx
export default async function SuccessPage({ searchParams }) {
const sessionId = searchParams.session_id;
// Don't trust the URL alone — verify with Stripe
const session = await stripe.checkout.sessions.retrieve(sessionId);
if (session.payment_status === 'paid') {
// Grant access immediately
await grantAccess(session.customer);
}
// Webhook will come later as backup
return <SuccessMessage />;
}.env.exampleprintfecho#!/bin/bash
# scripts/verify-external-integration.sh
SERVICE=$1
echo "Checking $SERVICE integration..."
# Check env vars
for var in ${SERVICE}_API_KEY ${SERVICE}_WEBHOOK_SECRET; do
if [ -z "${!var}" ]; then
echo "❌ Missing $var"
exit 1
fi
if [ "${!var}" != "$(echo "${!var}" | tr -d '\n')" ]; then
echo "❌ $var has trailing newline"
exit 1
fi
done
# Check health endpoint
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/health)
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ Health check failed (HTTP $HTTP_CODE)"
exit 1
fi
echo "✅ $SERVICE integration checks passed"// ❌ BAD: Silent failure on missing config
const apiKey = process.env.API_KEY || '';
// ❌ BAD: No context in error log
catch (e) { console.log('Error'); throw e; }
// ❌ BAD: Trusting webhook without verification
const event = JSON.parse(body); // No signature check!
// ❌ BAD: 100% reliance on webhooks
// If webhook fails, user never gets access
// ❌ BAD: No logging of received events
// Debugging nightmare when things go wrongstripe.webhooks.constructEvent()customer_creationpaymentsetupCONVEX_WEBHOOK_TOKEN