external-integration-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExternal Integration Patterns
外部服务集成模式
Patterns for reliable external service integration.
可靠的外部服务集成模式指南。
Triggers
触发场景
Invoke this skill when:
- File path contains ,
webhook,api/services/ - Code imports external service SDKs (stripe, @clerk, @sendgrid, etc.)
- Env vars reference external services
- Implementing any third-party API integration
- Reviewing webhook handlers
在以下场景中使用本指南:
- 文件路径包含、
webhook、api/services/ - 代码引入了外部服务SDK(stripe、@clerk、@sendgrid等)
- 环境变量引用了外部服务
- 实现任何第三方API集成
- 审核Webhook处理器
Core Principle
核心原则
External services fail. Your integration must be observable, recoverable, and fail loudly.
Silent failures are the worst failures. When Stripe doesn't deliver a webhook, when Clerk JWT validation fails, when Sendgrid rejects an email — you need to know immediately, not when a user complains.
外部服务可能会故障。你的集成必须具备可观测性、可恢复性,并且在故障时及时告警。
静默故障是最糟糕的故障类型。当Stripe未推送Webhook、Clerk的JWT验证失败、Sendgrid拒绝邮件发送时——你需要立即知晓,而不是等用户投诉才发现。
Required Patterns
必备模式
1. Fail-Fast Env Validation
1. 快速失败的环境验证
Validate environment variables at module load, not at runtime. Fail immediately with a clear message.
typescript
// 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!;Why this matters:
- Deploy fails immediately if config is wrong
- Error message tells you exactly what's missing
- No silent failures at 3am when a customer tries to checkout
在模块加载时验证环境变量,而非运行时。一旦发现问题立即抛出清晰的错误信息。
typescript
// 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!;重要性:
- 配置错误时部署会立即失败
- 错误信息明确告知缺失内容
- 避免凌晨3点用户尝试结账时出现静默故障
2. Health Check Endpoint
2. 健康检查端点
Every external service should have a health check endpoint.
typescript
// /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 });
}每个外部服务都应对应一个健康检查端点。
typescript
// /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 });
}3. Structured Error Logging
3. 结构化错误日志
Log every external service failure with full context.
typescript
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;
}Required fields:
- : Which external service (stripe, clerk, sendgrid)
service - : What you were trying to do
operation - : Who this affects (for debugging)
userId - : The error message
error - : When it happened
timestamp
记录每一次外部服务故障的完整上下文。
typescript
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;
}必填字段:
- : 涉及的外部服务(stripe、clerk、sendgrid)
service - : 执行的操作
operation - : 受影响的用户ID(用于调试)
userId - : 错误信息
error - : 故障发生时间
timestamp
4. Webhook Reliability
4. Webhook可靠性
Webhooks are inherently unreliable. Build for this reality.
typescript
export 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 });
}Webhook本质上不可靠,需针对这一特性进行构建。
typescript
export 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 });
}5. Reconciliation Cron (Safety Net)
5. 对账定时任务(安全保障)
Don't rely 100% on webhooks. Periodically sync state as a backup.
typescript
// 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()
}));
}不要100%依赖Webhook。定期同步状态作为备份。
typescript
// 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()
}));
}6. Pull-on-Success Activation
6. 成功后拉取激活
Don't wait for webhook to grant access. Verify payment immediately after redirect.
typescript
// /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 />;
}不要等待Webhook来授予权限。在跳转后立即验证支付状态。
typescript
// /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 />;
}Pre-Deploy Checklist
部署前检查清单
Before deploying any external integration:
部署任何外部集成前需完成:
Environment Variables
环境变量
- All required vars in
.env.example - Vars set on both dev and prod deployments
- No trailing whitespace (use , not
printf)echo - Format validated (sk_, whsec_, pk_*)
- 所有必填变量已添加到
.env.example - 开发和生产环境均已配置变量
- 变量无尾随空格(使用而非
printf设置)echo - 格式已验证(sk_、whsec_、pk_*等)
Webhook Configuration
Webhook配置
- Webhook URL uses canonical domain (no redirects)
- Secret matches between service dashboard and env vars
- Signature verification in handler
- Events logged before processing
- Webhook URL使用标准域名(无重定向)
- 服务控制台与环境变量中的密钥完全匹配
- 处理器中已实现签名验证
- 事件在处理前已记录
Observability
可观测性
- Health check endpoint exists
- Error paths log with context
- Monitoring/alerting configured
- 已存在健康检查端点
- 错误路径已记录上下文信息
- 已配置监控/告警
Reliability
可靠性
- Reconciliation cron or pull-on-success pattern
- Idempotency for duplicate events
- Graceful handling of service downtime
- 已实现对账定时任务或成功后拉取模式
- 重复事件已实现幂等性处理
- 服务停机时已实现优雅处理
Quick Verification Script
快速验证脚本
bash
#!/bin/bashbash
#!/bin/bashscripts/verify-external-integration.sh
scripts/verify-external-integration.sh
SERVICE=$1
echo "Checking $SERVICE integration..."
SERVICE=$1
echo "Checking $SERVICE integration..."
Check env vars
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
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
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"
undefinedHTTP_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"
undefinedAnti-Patterns to Avoid
需避免的反模式
typescript
// ❌ 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 wrongtypescript
// ❌ 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 wrongAPI Format Research (Before Integration)
API格式调研(集成前)
Before writing integration code, verify format compatibility:
- Check official docs for supported formats/encodings
- Verify your input format is in the supported list
- If not, plan conversion strategy upfront
Common format gotchas:
- Deepgram STT: No CAF support (use WAV, MP3, FLAC)
- Speech APIs: Prefer WAV/MP3 over platform-specific formats (CAF, HEIC)
- Image APIs: Check color space requirements (RGB vs CMYK)
编写集成代码前,需验证格式兼容性:
- 查阅官方文档确认支持的格式/编码
- 验证你的输入格式在支持列表中
- 若不支持,提前规划转换策略
常见格式陷阱:
- Deepgram STT:不支持CAF格式(使用WAV、MP3、FLAC)
- 语音API:优先使用WAV/MP3而非平台特定格式(CAF、HEIC)
- 图片API:检查色彩空间要求(RGB vs CMYK)
Service-Specific Notes
各服务特定说明
Stripe
Stripe
- Use for signature verification
stripe.webhooks.constructEvent() - Check Stripe Dashboard > Developers > Webhooks for delivery logs
- param only valid in
customer_creation/paymentmodesetup
- 使用进行签名验证
stripe.webhooks.constructEvent() - 在Stripe控制台>开发者>Webhooks中查看推送日志
- 参数仅在
customer_creation/payment模式下有效setup
Clerk
Clerk
- must match exactly between Clerk and Convex
CONVEX_WEBHOOK_TOKEN - JWT template names are case-sensitive
- Webhook URL must not redirect
- 在Clerk和Convex之间必须完全匹配
CONVEX_WEBHOOK_TOKEN - JWT模板名称区分大小写
- Webhook URL不得重定向
Sendgrid
Sendgrid
- Verify sender domain before going live
- Inbound parse webhooks need signature verification
- Rate limits apply — implement queuing for bulk sends
- 上线前需验证发件人域名
- 入站解析Webhook需进行签名验证
- 存在速率限制——批量发送需实现队列机制