external-integration-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

External 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:
  • service
    : Which external service (stripe, clerk, sendgrid)
  • operation
    : What you were trying to do
  • userId
    : Who this affects (for debugging)
  • error
    : The error message
  • timestamp
    : When it happened
记录每一次外部服务故障的完整上下文。
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;
}
必填字段:
  • service
    : 涉及的外部服务(stripe、clerk、sendgrid)
  • operation
    : 执行的操作
  • userId
    : 受影响的用户ID(用于调试)
  • 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
    printf
    , not
    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/bash
bash
#!/bin/bash

scripts/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"
undefined
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"
undefined

Anti-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 wrong
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 wrong

API Format Research (Before Integration)

API格式调研(集成前)

Before writing integration code, verify format compatibility:
  1. Check official docs for supported formats/encodings
  2. Verify your input format is in the supported list
  3. 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)
编写集成代码前,需验证格式兼容性:
  1. 查阅官方文档确认支持的格式/编码
  2. 验证你的输入格式在支持列表中
  3. 若不支持,提前规划转换策略
常见格式陷阱:
  • Deepgram STT:不支持CAF格式(使用WAV、MP3、FLAC)
  • 语音API:优先使用WAV/MP3而非平台特定格式(CAF、HEIC)
  • 图片API:检查色彩空间要求(RGB vs CMYK)

Service-Specific Notes

各服务特定说明

Stripe

Stripe

  • Use
    stripe.webhooks.constructEvent()
    for signature verification
  • Check Stripe Dashboard > Developers > Webhooks for delivery logs
  • customer_creation
    param only valid in
    payment
    /
    setup
    mode
  • 使用
    stripe.webhooks.constructEvent()
    进行签名验证
  • 在Stripe控制台>开发者>Webhooks中查看推送日志
  • customer_creation
    参数仅在
    payment
    /
    setup
    模式下有效

Clerk

Clerk

  • CONVEX_WEBHOOK_TOKEN
    must match exactly between Clerk and Convex
  • JWT template names are case-sensitive
  • Webhook URL must not redirect
  • CONVEX_WEBHOOK_TOKEN
    在Clerk和Convex之间必须完全匹配
  • 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需进行签名验证
  • 存在速率限制——批量发送需实现队列机制