api-integration-builder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI Integration Builder
API集成构建器
Build reliable, maintainable integrations with third-party APIs.
构建可靠、可维护的第三方API集成。
Core Principles
核心原则
- Assume failure: APIs will go down, rate limits will hit, data will be inconsistent
- Idempotency matters: Retries shouldn't cause duplicate actions
- User experience first: Never show users "API Error 429"
- Security always: Tokens are secrets, validate all data, assume malicious input
- 假设故障存在:API会宕机、会触发速率限制、数据会出现不一致
- 幂等性至关重要:重试操作不应导致重复行为
- 用户体验优先:绝不要向用户展示“API Error 429”这类提示
- 安全永远第一:令牌是机密信息,验证所有数据,假设输入是恶意的
Integration Architecture
集成架构
Basic Integration Flow
基础集成流程
Your App ←→ Integration Layer ←→ Third-Party API
├── Auth (OAuth, API keys)
├── Rate limiting
├── Retries
├── Error handling
├── Data transformation
└── Webhooks (if supported)你的应用 ←→ 集成层 ←→ 第三方API
├── 认证(OAuth、API密钥)
├── 速率限制
├── 重试机制
├── 错误处理
├── 数据转换
└── Webhook(如支持)Components
组件
- Authentication Layer: Handle OAuth, refresh tokens, API keys
- Request Manager: Make API calls with retries, rate limiting
- Webhook Handler: Receive real-time updates from third parties
- Data Sync: Keep your data in sync with external service
- Error Recovery: Handle failures gracefully
- 认证层:处理OAuth、刷新令牌、API密钥
- 请求管理器:执行带有重试、速率限制的API调用
- Webhook处理器:接收来自第三方的实时更新
- 数据同步:保持你的数据与外部服务同步
- 错误恢复:优雅处理故障
Authentication Patterns
认证模式
API Key Authentication
API密钥认证
Simple but limited:
typescript
interface APIKeyConfig {
api_key: string
api_secret?: string
}
class SimpleAPIClient {
private apiKey: string
async request(endpoint: string, options: RequestOptions) {
return fetch(`https://api.service.com${endpoint}`, {
...options,
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
})
}
}Pros: Simple, no complex flows
Cons: Can't act on behalf of users, no granular permissions
简单但有局限:
typescript
interface APIKeyConfig {
api_key: string
api_secret?: string
}
class SimpleAPIClient {
private apiKey: string
async request(endpoint: string, options: RequestOptions) {
return fetch(`https://api.service.com${endpoint}`, {
...options,
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
})
}
}优点:实现简单,无复杂流程
缺点:无法代表用户操作,无细粒度权限控制
OAuth 2.0 Flow
OAuth 2.0流程
The standard for user-authorized access:
typescript
// 1. Redirect user to authorize
app.get('/connect/slack', (req, res) => {
const authUrl = new URL('https://slack.com/oauth/v2/authorize')
authUrl.searchParams.set('client_id', SLACK_CLIENT_ID)
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/auth/slack/callback')
authUrl.searchParams.set('scope', 'channels:read,chat:write')
authUrl.searchParams.set('state', generateSecureRandomString()) // CSRF protection
res.redirect(authUrl.toString())
})
// 2. Handle callback
app.get('/auth/slack/callback', async (req, res) => {
const { code, state } = req.query
// Verify state to prevent CSRF
if (state !== req.session.oauthState) {
throw new Error('Invalid state')
}
// Exchange code for access token
const tokenResponse = await fetch('https://slack.com/api/oauth.v2.access', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: SLACK_CLIENT_ID,
client_secret: SLACK_CLIENT_SECRET,
code: code,
redirect_uri: 'https://yourapp.com/auth/slack/callback'
})
})
const { access_token, refresh_token, expires_in } = await tokenResponse.json()
// Store tokens securely (encrypted!)
await db.storeIntegration({
user_id: req.user.id,
service: 'slack',
access_token: encrypt(access_token),
refresh_token: encrypt(refresh_token),
expires_at: Date.now() + expires_in * 1000
})
res.redirect('/settings/integrations?success=slack')
})Token Refresh:
typescript
async function getValidAccessToken(userId: string, service: string) {
const integration = await db.getIntegration(userId, service)
// Token still valid?
if (integration.expires_at > Date.now() + 60000) {
// 1 min buffer
return decrypt(integration.access_token)
}
// Refresh token
const refreshResponse = await fetch('https://slack.com/api/oauth.v2.access', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: SLACK_CLIENT_ID,
client_secret: SLACK_CLIENT_SECRET,
grant_type: 'refresh_token',
refresh_token: decrypt(integration.refresh_token)
})
})
const { access_token, expires_in } = await refreshResponse.json()
// Update stored tokens
await db.updateIntegration(integration.id, {
access_token: encrypt(access_token),
expires_at: Date.now() + expires_in * 1000
})
return access_token
}用户授权访问的标准方案:
typescript
// 1. 重定向用户至授权页面
app.get('/connect/slack', (req, res) => {
const authUrl = new URL('https://slack.com/oauth/v2/authorize')
authUrl.searchParams.set('client_id', SLACK_CLIENT_ID)
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/auth/slack/callback')
authUrl.searchParams.set('scope', 'channels:read,chat:write')
authUrl.searchParams.set('state', generateSecureRandomString()) // CSRF防护
res.redirect(authUrl.toString())
})
// 2. 处理回调
app.get('/auth/slack/callback', async (req, res) => {
const { code, state } = req.query
// 验证state以防止CSRF攻击
if (state !== req.session.oauthState) {
throw new Error('Invalid state')
}
// 交换授权码获取访问令牌
const tokenResponse = await fetch('https://slack.com/api/oauth.v2.access', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: SLACK_CLIENT_ID,
client_secret: SLACK_CLIENT_SECRET,
code: code,
redirect_uri: 'https://yourapp.com/auth/slack/callback'
})
})
const { access_token, refresh_token, expires_in } = await tokenResponse.json()
// 安全存储令牌(加密!)
await db.storeIntegration({
user_id: req.user.id,
service: 'slack',
access_token: encrypt(access_token),
refresh_token: encrypt(refresh_token),
expires_at: Date.now() + expires_in * 1000
})
res.redirect('/settings/integrations?success=slack')
})令牌刷新:
typescript
async function getValidAccessToken(userId: string, service: string) {
const integration = await db.getIntegration(userId, service)
// 令牌是否仍有效?
if (integration.expires_at > Date.now() + 60000) {
// 预留1分钟缓冲时间
return decrypt(integration.access_token)
}
// 刷新令牌
const refreshResponse = await fetch('https://slack.com/api/oauth.v2.access', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: SLACK_CLIENT_ID,
client_secret: SLACK_CLIENT_SECRET,
grant_type: 'refresh_token',
refresh_token: decrypt(integration.refresh_token)
})
})
const { access_token, expires_in } = await refreshResponse.json()
// 更新存储的令牌
await db.updateIntegration(integration.id, {
access_token: encrypt(access_token),
expires_at: Date.now() + expires_in * 1000
})
return access_token
}Request Management
请求管理
Rate Limiting
速率限制
Client-side rate limiting:
typescript
import { RateLimiter } from 'limiter'
class RateLimitedAPIClient {
private limiter: RateLimiter
constructor(tokensPerInterval: number, interval: string) {
this.limiter = new RateLimiter({
tokensPerInterval,
interval
})
}
async request(endpoint: string, options: RequestOptions) {
// Wait for rate limit token
await this.limiter.removeTokens(1)
return fetch(`https://api.service.com${endpoint}`, options)
}
}
// Example: Slack allows ~1 request per second
const slackClient = new RateLimitedAPIClient(1, 'second')429 Response handling:
typescript
async function requestWithRetry(url: string, options: RequestOptions, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options)
if (response.status === 429) {
// Check Retry-After header
const retryAfter = response.headers.get('Retry-After')
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000
console.log(`Rate limited, waiting ${waitTime}ms`)
await sleep(waitTime)
continue
}
return response
}
throw new Error('Max retries exceeded')
}客户端速率限制:
typescript
import { RateLimiter } from 'limiter'
class RateLimitedAPIClient {
private limiter: RateLimiter
constructor(tokensPerInterval: number, interval: string) {
this.limiter = new RateLimiter({
tokensPerInterval,
interval
})
}
async request(endpoint: string, options: RequestOptions) {
// 等待速率限制令牌
await this.limiter.removeTokens(1)
return fetch(`https://api.service.com${endpoint}`, options)
}
}
// 示例:Slack允许约每秒1次请求
const slackClient = new RateLimitedAPIClient(1, 'second')429响应处理:
typescript
async function requestWithRetry(url: string, options: RequestOptions, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options)
if (response.status === 429) {
// 检查Retry-After响应头
const retryAfter = response.headers.get('Retry-After')
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000
console.log(`触发速率限制,等待${waitTime}ms`)
await sleep(waitTime)
continue
}
return response
}
throw new Error('超出最大重试次数')
}Error Handling
错误处理
Comprehensive error handling:
typescript
class APIError extends Error {
constructor(
public statusCode: number,
public response: any,
public retryable: boolean
) {
super(`API Error: ${statusCode}`)
}
}
async function safeAPIRequest(endpoint: string, options: RequestOptions) {
try {
const response = await fetch(endpoint, options)
// Success
if (response.ok) {
return await response.json()
}
// Client errors (4xx) - usually not retryable
if (response.status >= 400 && response.status < 500) {
if (response.status === 401) {
// Token expired, refresh and retry
await refreshAccessToken()
return safeAPIRequest(endpoint, options)
}
if (response.status === 429) {
// Rate limited, retry with backoff
throw new APIError(429, await response.json(), true)
}
// Other 4xx errors - don't retry
throw new APIError(response.status, await response.json(), false)
}
// Server errors (5xx) - retryable
if (response.status >= 500) {
throw new APIError(response.status, await response.json(), true)
}
} catch (error) {
if (error instanceof APIError) throw error
// Network errors - retryable
throw new APIError(0, { message: error.message }, true)
}
}Exponential backoff:
typescript
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
if (error instanceof APIError && !error.retryable) {
throw error // Don't retry non-retryable errors
}
if (attempt === maxRetries - 1) {
throw error // Last attempt failed
}
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5)
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`)
await sleep(delay)
}
}
throw new Error('Unreachable')
}
// Usage
const data = await retryWithBackoff(() => slackClient.postMessage('#general', 'Hello!'))全面错误处理:
typescript
class APIError extends Error {
constructor(
public statusCode: number,
public response: any,
public retryable: boolean
) {
super(`API错误: ${statusCode}`)
}
}
async function safeAPIRequest(endpoint: string, options: RequestOptions) {
try {
const response = await fetch(endpoint, options)
// 请求成功
if (response.ok) {
return await response.json()
}
// 客户端错误(4xx)- 通常不可重试
if (response.status >= 400 && response.status < 500) {
if (response.status === 401) {
// 令牌过期,刷新后重试
await refreshAccessToken()
return safeAPIRequest(endpoint, options)
}
if (response.status === 429) {
// 触发速率限制,退避后重试
throw new APIError(429, await response.json(), true)
}
// 其他4xx错误 - 不重试
throw new APIError(response.status, await response.json(), false)
}
// 服务端错误(5xx)- 可重试
if (response.status >= 500) {
throw new APIError(response.status, await response.json(), true)
}
} catch (error) {
if (error instanceof APIError) throw error
// 网络错误 - 可重试
throw new APIError(0, { message: error.message }, true)
}
}指数退避:
typescript
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
if (error instanceof APIError && !error.retryable) {
throw error // 不可重试的错误直接抛出
}
if (attempt === maxRetries - 1) {
throw error // 最后一次尝试失败
}
// 带抖动的指数退避
const delay = baseDelay * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5)
console.log(`第${attempt + 1}次尝试失败,${delay}ms后重试`)
await sleep(delay)
}
}
throw new Error('无法到达此处')
}
// 使用示例
const data = await retryWithBackoff(() => slackClient.postMessage('#general', 'Hello!'))Webhook Handling
Webhook处理
Receiving Webhooks
接收Webhook
typescript
interface WebhookPayload {
event_type: string
data: any
timestamp: number
signature: string
}
app.post('/webhooks/stripe', async (req, res) => {
// 1. Verify signature (CRITICAL for security)
const signature = req.headers['stripe-signature']
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(req.body, signature, STRIPE_WEBHOOK_SECRET)
} catch (error) {
console.error('⚠️ Webhook signature verification failed:', error.message)
return res.status(400).send('Webhook signature verification failed')
}
// 2. Respond immediately (don't make webhook wait)
res.status(200).send('Received')
// 3. Process asynchronously
await queue.add('process-webhook', {
event_type: event.type,
event_id: event.id,
data: event.data
})
})
// Process webhook in background job
async function processWebhook(job: Job) {
const { event_type, event_id, data } = job.data
// Idempotency check (handle duplicate webhooks)
const existing = await db.webhookEvents.findOne({ event_id })
if (existing) {
console.log(`Webhook ${event_id} already processed`)
return
}
// Mark as processing
await db.webhookEvents.create({ event_id, status: 'processing' })
try {
switch (event_type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(data)
break
case 'customer.subscription.deleted':
await handleSubscriptionCancelled(data)
break
// ... other event types
}
// Mark as completed
await db.webhookEvents.update(event_id, { status: 'completed' })
} catch (error) {
// Mark as failed, will retry
await db.webhookEvents.update(event_id, {
status: 'failed',
error: error.message,
retry_count: (existing?.retry_count || 0) + 1
})
throw error // Let queue retry
}
}typescript
interface WebhookPayload {
event_type: string
data: any
timestamp: number
signature: string
}
app.post('/webhooks/stripe', async (req, res) => {
// 1. 验证签名(安全性至关重要)
const signature = req.headers['stripe-signature']
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(req.body, signature, STRIPE_WEBHOOK_SECRET)
} catch (error) {
console.error('⚠️ Webhook签名验证失败:', error.message)
return res.status(400).send('Webhook签名验证失败')
}
// 2. 立即响应(不要让Webhook等待)
res.status(200).send('已接收')
// 3. 异步处理
await queue.add('process-webhook', {
event_type: event.type,
event_id: event.id,
data: event.data
})
})
// 后台任务处理Webhook
async function processWebhook(job: Job) {
const { event_type, event_id, data } = job.data
// 幂等性检查(处理重复Webhook)
const existing = await db.webhookEvents.findOne({ event_id })
if (existing) {
console.log(`Webhook ${event_id}已处理`)
return
}
// 标记为处理中
await db.webhookEvents.create({ event_id, status: 'processing' })
try {
switch (event_type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(data)
break
case 'customer.subscription.deleted':
await handleSubscriptionCancelled(data)
break
// ... 其他事件类型
}
// 标记为处理完成
await db.webhookEvents.update(event_id, { status: 'completed' })
} catch (error) {
// 标记为失败,将重试
await db.webhookEvents.update(event_id, {
status: 'failed',
error: error.message,
retry_count: (existing?.retry_count || 0) + 1
})
throw error // 让队列重试
}
}Webhook Security
Webhook安全
typescript
// HMAC signature verification
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
const expectedSignature = crypto.createHmac('sha256', secret).update(payload).digest('hex')
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))
}
// Timestamp validation (prevent replay attacks)
function validateWebhookTimestamp(timestamp: number, toleranceSeconds = 300) {
const now = Math.floor(Date.now() / 1000)
return Math.abs(now - timestamp) < toleranceSeconds
}typescript
// HMAC签名验证
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
const expectedSignature = crypto.createHmac('sha256', secret).update(payload).digest('hex')
// 恒时比较以防止时序攻击
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))
}
// 时间戳验证(防止重放攻击)
function validateWebhookTimestamp(timestamp: number, toleranceSeconds = 300) {
const now = Math.floor(Date.now() / 1000)
return Math.abs(now - timestamp) < toleranceSeconds
}Data Synchronization
数据同步
Sync Strategy
同步策略
typescript
interface SyncStrategy {
// Full sync: Get all data from API
fullSync(): Promise<void>
// Incremental sync: Get only changed data since last sync
incrementalSync(since: Date): Promise<void>
// Real-time sync: Use webhooks for instant updates
realTimeSync(webhookData: any): Promise<void>
}
class GoogleCalendarSync implements SyncStrategy {
async fullSync() {
const calendars = await googleCalendar.list()
for (const calendar of calendars) {
const events = await googleCalendar.events.list({
calendarId: calendar.id,
maxResults: 2500
})
await db.events.bulkUpsert(events)
}
}
async incrementalSync(since: Date) {
const events = await googleCalendar.events.list({
updatedMin: since.toISOString(),
showDeleted: true // Important: track deletions
})
for (const event of events) {
if (event.status === 'cancelled') {
await db.events.delete(event.id)
} else {
await db.events.upsert(event)
}
}
}
async realTimeSync(webhookData: any) {
// Google sends channel notifications
const { resourceId, resourceUri } = webhookData
// Fetch the changed resource
const event = await googleCalendar.events.get(resourceId)
await db.events.upsert(event)
}
}typescript
interface SyncStrategy {
// 全量同步:从API获取所有数据
fullSync(): Promise<void>
// 增量同步:仅获取自上次同步以来变更的数据
incrementalSync(since: Date): Promise<void>
// 实时同步:使用Webhook获取即时更新
realTimeSync(webhookData: any): Promise<void>
}
class GoogleCalendarSync implements SyncStrategy {
async fullSync() {
const calendars = await googleCalendar.list()
for (const calendar of calendars) {
const events = await googleCalendar.events.list({
calendarId: calendar.id,
maxResults: 2500
})
await db.events.bulkUpsert(events)
}
}
async incrementalSync(since: Date) {
const events = await googleCalendar.events.list({
updatedMin: since.toISOString(),
showDeleted: true // 重要:跟踪删除操作
})
for (const event of events) {
if (event.status === 'cancelled') {
await db.events.delete(event.id)
} else {
await db.events.upsert(event)
}
}
}
async realTimeSync(webhookData: any) {
// Google发送频道通知
const { resourceId, resourceUri } = webhookData
// 获取变更的资源
const event = await googleCalendar.events.get(resourceId)
await db.events.upsert(event)
}
}Sync Scheduler
同步调度器
typescript
interface SyncJob {
user_id: string
service: string
type: 'full' | 'incremental'
}
// Schedule sync jobs
async function scheduleSyncJobs() {
// Full sync: Weekly for all active integrations
cron.schedule('0 0 * * 0', async () => {
const integrations = await db.integrations.findActive()
for (const integration of integrations) {
await queue.add('sync', {
user_id: integration.user_id,
service: integration.service,
type: 'full'
})
}
})
// Incremental sync: Every 15 minutes
cron.schedule('*/15 * * * *', async () => {
const integrations = await db.integrations.findActive()
for (const integration of integrations) {
await queue.add('sync', {
user_id: integration.user_id,
service: integration.service,
type: 'incremental'
})
}
})
}
// Process sync job
async function processSyncJob(job: Job<SyncJob>) {
const { user_id, service, type } = job.data
const sync = getSyncStrategy(service)
if (type === 'full') {
await sync.fullSync()
} else {
const lastSync = await db.syncLog.getLastSync(user_id, service)
await sync.incrementalSync(lastSync.completed_at)
}
await db.syncLog.create({
user_id,
service,
type,
completed_at: new Date()
})
}typescript
interface SyncJob {
user_id: string
service: string
type: 'full' | 'incremental'
}
// 调度同步任务
async function scheduleSyncJobs() {
// 全量同步:每周对所有活跃集成执行一次
cron.schedule('0 0 * * 0', async () => {
const integrations = await db.integrations.findActive()
for (const integration of integrations) {
await queue.add('sync', {
user_id: integration.user_id,
service: integration.service,
type: 'full'
})
}
})
// 增量同步:每15分钟执行一次
cron.schedule('*/15 * * * *', async () => {
const integrations = await db.integrations.findActive()
for (const integration of integrations) {
await queue.add('sync', {
user_id: integration.user_id,
service: integration.service,
type: 'incremental'
})
}
})
}
// 处理同步任务
async function processSyncJob(job: Job<SyncJob>) {
const { user_id, service, type } = job.data
const sync = getSyncStrategy(service)
if (type === 'full') {
await sync.fullSync()
} else {
const lastSync = await db.syncLog.getLastSync(user_id, service)
await sync.incrementalSync(lastSync.completed_at)
}
await db.syncLog.create({
user_id,
service,
type,
completed_at: new Date()
})
}Common Integration Patterns
常见集成模式
Slack Integration
Slack集成
typescript
class SlackIntegration {
async postMessage(channel: string, text: string, attachments?: any[]) {
const accessToken = await getValidAccessToken(userId, 'slack')
return await retryWithBackoff(() =>
fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel,
text,
attachments
})
})
)
}
async getChannels() {
const accessToken = await getValidAccessToken(userId, 'slack')
const response = await fetch('https://slack.com/api/conversations.list', {
headers: { Authorization: `Bearer ${accessToken}` }
})
const data = await response.json()
if (!data.ok) {
throw new Error(`Slack API error: ${data.error}`)
}
return data.channels
}
}typescript
class SlackIntegration {
async postMessage(channel: string, text: string, attachments?: any[]) {
const accessToken = await getValidAccessToken(userId, 'slack')
return await retryWithBackoff(() =>
fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel,
text,
attachments
})
})
)
}
async getChannels() {
const accessToken = await getValidAccessToken(userId, 'slack')
const response = await fetch('https://slack.com/api/conversations.list', {
headers: { Authorization: `Bearer ${accessToken}` }
})
const data = await response.json()
if (!data.ok) {
throw new Error(`Slack API错误: ${data.error}`)
}
return data.channels
}
}Stripe Integration
Stripe集成
typescript
class StripeIntegration {
async createSubscription(customerId: string, priceId: string) {
try {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
metadata: {
user_id: userId,
plan: 'pro'
}
})
return {
subscription_id: subscription.id,
client_secret: subscription.latest_invoice.payment_intent.client_secret
}
} catch (error) {
if (error instanceof Stripe.errors.StripeError) {
// Handle specific Stripe errors
if (error.type === 'card_error') {
throw new Error('Card was declined')
}
}
throw error
}
}
async cancelSubscription(subscriptionId: string) {
// Cancel at period end (don't refund)
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true
})
}
}typescript
class StripeIntegration {
async createSubscription(customerId: string, priceId: string) {
try {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
metadata: {
user_id: userId,
plan: 'pro'
}
})
return {
subscription_id: subscription.id,
client_secret: subscription.latest_invoice.payment_intent.client_secret
}
} catch (error) {
if (error instanceof Stripe.errors.StripeError) {
// 处理特定Stripe错误
if (error.type === 'card_error') {
throw new Error('卡片被拒绝')
}
}
throw error
}
}
async cancelSubscription(subscriptionId: string) {
// 计费周期结束时取消(不退款)
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true
})
}
}Gmail/Google Calendar Integration
Gmail/Google Calendar集成
typescript
import { google } from 'googleapis'
class GoogleIntegration {
async sendEmail(to: string, subject: string, body: string) {
const auth = await this.getOAuth2Client()
const gmail = google.gmail({ version: 'v1', auth })
const message = [`To: ${to}`, `Subject: ${subject}`, '', body].join('\n')
const encodedMessage = Buffer.from(message)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
await gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: encodedMessage
}
})
}
async listCalendarEvents(calendarId: string, timeMin: Date, timeMax: Date) {
const auth = await this.getOAuth2Client()
const calendar = google.calendar({ version: 'v3', auth })
const response = await calendar.events.list({
calendarId,
timeMin: timeMin.toISOString(),
timeMax: timeMax.toISOString(),
singleEvents: true,
orderBy: 'startTime'
})
return response.data.items
}
}typescript
import { google } from 'googleapis'
class GoogleIntegration {
async sendEmail(to: string, subject: string, body: string) {
const auth = await this.getOAuth2Client()
const gmail = google.gmail({ version: 'v1', auth })
const message = [`To: ${to}`, `Subject: ${subject}`, '', body].join('\n')
const encodedMessage = Buffer.from(message)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
await gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: encodedMessage
}
})
}
async listCalendarEvents(calendarId: string, timeMin: Date, timeMax: Date) {
const auth = await this.getOAuth2Client()
const calendar = google.calendar({ version: 'v3', auth })
const response = await calendar.events.list({
calendarId,
timeMin: timeMin.toISOString(),
timeMax: timeMax.toISOString(),
singleEvents: true,
orderBy: 'startTime'
})
return response.data.items
}
}Testing Integrations
集成测试
Mock External APIs
模拟外部API
typescript
// tests/mocks/stripe.ts
export class MockStripe {
subscriptions = {
create: jest.fn().mockResolvedValue({
id: 'sub_123',
status: 'active'
}),
cancel: jest.fn().mockResolvedValue({
id: 'sub_123',
status: 'canceled'
})
}
}
// tests/integration.test.ts
describe('Stripe Integration', () => {
let stripeIntegration: StripeIntegration
beforeEach(() => {
stripe = new MockStripe()
stripeIntegration = new StripeIntegration(stripe)
})
it('creates a subscription', async () => {
const result = await stripeIntegration.createSubscription('cus_123', 'price_123')
expect(result.subscription_id).toBe('sub_123')
expect(stripe.subscriptions.create).toHaveBeenCalledWith({
customer: 'cus_123',
items: [{ price: 'price_123' }]
// ...
})
})
it('handles card errors gracefully', async () => {
stripe.subscriptions.create.mockRejectedValue(
new Stripe.errors.StripeCardError('Card declined')
)
await expect(stripeIntegration.createSubscription('cus_123', 'price_123')).rejects.toThrow(
'Card was declined'
)
})
})typescript
// tests/mocks/stripe.ts
export class MockStripe {
subscriptions = {
create: jest.fn().mockResolvedValue({
id: 'sub_123',
status: 'active'
}),
cancel: jest.fn().mockResolvedValue({
id: 'sub_123',
status: 'canceled'
})
}
}
// tests/integration.test.ts
describe('Stripe Integration', () => {
let stripeIntegration: StripeIntegration
beforeEach(() => {
stripe = new MockStripe()
stripeIntegration = new StripeIntegration(stripe)
})
it('创建订阅', async () => {
const result = await stripeIntegration.createSubscription('cus_123', 'price_123')
expect(result.subscription_id).toBe('sub_123')
expect(stripe.subscriptions.create).toHaveBeenCalledWith({
customer: 'cus_123',
items: [{ price: 'price_123' }]
// ...
})
})
it('优雅处理卡片错误', async () => {
stripe.subscriptions.create.mockRejectedValue(
new Stripe.errors.StripeCardError('Card declined')
)
await expect(stripeIntegration.createSubscription('cus_123', 'price_123')).rejects.toThrow(
'卡片被拒绝'
)
})
})Webhook Testing
Webhook测试
typescript
// Generate valid webhook signatures for testing
function generateTestWebhook(payload: any, secret: string) {
const timestamp = Math.floor(Date.now() / 1000)
const signature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${JSON.stringify(payload)}`)
.digest('hex')
return {
payload,
headers: {
'stripe-signature': `t=${timestamp},v1=${signature}`
}
}
}
describe('Webhook Handler', () => {
it('processes valid webhook', async () => {
const webhook = generateTestWebhook(
{
type: 'payment_intent.succeeded',
data: {
/* ... */
}
},
STRIPE_WEBHOOK_SECRET
)
const response = await request(app)
.post('/webhooks/stripe')
.set(webhook.headers)
.send(webhook.payload)
expect(response.status).toBe(200)
// Verify processing happened
})
it('rejects invalid signature', async () => {
const response = await request(app)
.post('/webhooks/stripe')
.set({ 'stripe-signature': 'invalid' })
.send({ type: 'payment_intent.succeeded' })
expect(response.status).toBe(400)
})
})typescript
// 生成测试用的有效Webhook签名
function generateTestWebhook(payload: any, secret: string) {
const timestamp = Math.floor(Date.now() / 1000)
const signature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${JSON.stringify(payload)}`)
.digest('hex')
return {
payload,
headers: {
'stripe-signature': `t=${timestamp},v1=${signature}`
}
}
}
describe('Webhook Handler', () => {
it('处理有效Webhook', async () => {
const webhook = generateTestWebhook(
{
type: 'payment_intent.succeeded',
data: {
/* ... */
}
},
STRIPE_WEBHOOK_SECRET
)
const response = await request(app)
.post('/webhooks/stripe')
.set(webhook.headers)
.send(webhook.payload)
expect(response.status).toBe(200)
// 验证处理已执行
})
it('拒绝无效签名', async () => {
const response = await request(app)
.post('/webhooks/stripe')
.set({ 'stripe-signature': 'invalid' })
.send({ type: 'payment_intent.succeeded' })
expect(response.status).toBe(400)
})
})Monitoring & Observability
监控与可观测性
Integration Health Checks
集成健康检查
typescript
interface IntegrationHealth {
service: string
status: 'healthy' | 'degraded' | 'down'
last_success: Date
last_failure?: Date
error_rate_24h: number
}
async function checkIntegrationHealth(service: string): Promise<IntegrationHealth> {
const logs = await db.integrationLogs.findRecent(service, '24h')
const total = logs.length
const failures = logs.filter(l => l.status === 'failed').length
const errorRate = failures / total
let status: 'healthy' | 'degraded' | 'down'
if (errorRate < 0.01) status = 'healthy'
else if (errorRate < 0.1) status = 'degraded'
else status = 'down'
return {
service,
status,
last_success: logs.find(l => l.status === 'success')?.timestamp,
last_failure: logs.find(l => l.status === 'failed')?.timestamp,
error_rate_24h: errorRate
}
}typescript
interface IntegrationHealth {
service: string
status: 'healthy' | 'degraded' | 'down'
last_success: Date
last_failure?: Date
error_rate_24h: number
}
async function checkIntegrationHealth(service: string): Promise<IntegrationHealth> {
const logs = await db.integrationLogs.findRecent(service, '24h')
const total = logs.length
const failures = logs.filter(l => l.status === 'failed').length
const errorRate = failures / total
let status: 'healthy' | 'degraded' | 'down'
if (errorRate < 0.01) status = 'healthy'
else if (errorRate < 0.1) status = 'degraded'
else status = 'down'
return {
service,
status,
last_success: logs.find(l => l.status === 'success')?.timestamp,
last_failure: logs.find(l => l.status === 'failed')?.timestamp,
error_rate_24h: errorRate
}
}Logging & Alerts
日志与告警
typescript
// Log all API requests
async function logAPIRequest(
service: string,
endpoint: string,
statusCode: number,
duration: number,
error?: Error
) {
await db.apiLogs.create({
service,
endpoint,
status_code: statusCode,
duration_ms: duration,
error: error?.message,
timestamp: new Date()
})
// Alert on high error rates
if (statusCode >= 500) {
const recentErrors = await db.apiLogs.count({
service,
status_code: { $gte: 500 },
timestamp: { $gte: new Date(Date.now() - 5 * 60 * 1000) } // Last 5 min
})
if (recentErrors > 10) {
await sendAlert({
title: `High error rate for ${service} integration`,
message: `${recentErrors} errors in last 5 minutes`
})
}
}
}typescript
// 记录所有API请求
async function logAPIRequest(
service: string,
endpoint: string,
statusCode: number,
duration: number,
error?: Error
) {
await db.apiLogs.create({
service,
endpoint,
status_code: statusCode,
duration_ms: duration,
error: error?.message,
timestamp: new Date()
})
// 高错误率时触发告警
if (statusCode >= 500) {
const recentErrors = await db.apiLogs.count({
service,
status_code: { $gte: 500 },
timestamp: { $gte: new Date(Date.now() - 5 * 60 * 1000) } // 最近5分钟
})
if (recentErrors > 10) {
await sendAlert({
title: `${service}集成错误率过高`,
message: `最近5分钟内出现${recentErrors}次错误`
})
}
}
}User Experience
用户体验
Integration Status UI
集成状态UI
typescript
interface IntegrationStatus {
connected: boolean
last_sync: Date
sync_in_progress: boolean
error?: string
}
// Show integration status to user
<Card>
<IntegrationIcon service="slack" />
<div>
<h3>Slack</h3>
{status.connected ? (
<>
<Badge color="green">Connected</Badge>
<p>Last synced {formatRelative(status.last_sync)}</p>
{status.sync_in_progress && <Spinner />}
</>
) : (
<>
<Badge color="red">Disconnected</Badge>
{status.error && <p className="error">{status.error}</p>}
<Button onClick={reconnect}>Reconnect</Button>
</>
)}
</div>
<Button variant="secondary" onClick={disconnect}>
Disconnect
</Button>
</Card>typescript
interface IntegrationStatus {
connected: boolean
last_sync: Date
sync_in_progress: boolean
error?: string
}
// 向用户展示集成状态
<Card>
<IntegrationIcon service="slack" />
<div>
<h3>Slack</h3>
{status.connected ? (
<>
<Badge color="green">已连接</Badge>
<p>上次同步时间 {formatRelative(status.last_sync)}</p>
{status.sync_in_progress && <Spinner />}
</>
) : (
<>
<Badge color="red">已断开</Badge>
{status.error && <p className="error">{status.error}</p>}
<Button onClick={reconnect}>重新连接</Button>
</>
)}
</div>
<Button variant="secondary" onClick={disconnect}>
断开连接
</Button>
</Card>Quick Start Checklist
快速开始检查清单
Setting Up Your First Integration
搭建你的第一个集成
- Choose integration type (OAuth vs API key)
- Register OAuth app (get client ID/secret)
- Implement authentication flow
- Store tokens securely (encrypted)
- Implement token refresh
- Add rate limiting
- Add error handling with retries
- Set up webhook endpoint (if available)
- Add webhook signature verification
- Implement idempotency for webhooks
- Add monitoring & logging
- Test thoroughly (including failures)
- 选择集成类型(OAuth vs API密钥)
- 注册OAuth应用(获取客户端ID/密钥)
- 实现认证流程
- 安全存储令牌(加密)
- 实现令牌刷新逻辑
- 添加速率限制
- 添加带重试的错误处理
- 配置Webhook端点(如支持)
- 添加Webhook签名验证
- 实现Webhook幂等性处理
- 添加监控与日志
- 全面测试(包括故障场景)
Common Pitfalls
常见陷阱
❌ Storing tokens unencrypted: Always encrypt access/refresh tokens
❌ No token refresh: Tokens expire, implement refresh flow
❌ Synchronous webhook processing: Process webhooks in background jobs
❌ No idempotency: Webhooks may be delivered multiple times
❌ Ignoring rate limits: Implement client-side rate limiting
❌ Poor error messages: Show helpful errors, not "API Error 500"
❌ No retry logic: APIs are unreliable, always retry with backoff
❌ Missing signature verification: Attackers can forge webhooks
❌ 未加密存储令牌:始终对访问/刷新令牌进行加密
❌ 未实现令牌刷新:令牌会过期,需实现刷新流程
❌ 同步处理Webhook:在后台任务中处理Webhook
❌ 未实现幂等性:Webhook可能会被多次投递
❌ 忽略速率限制:实现客户端速率限制
❌ 错误提示不友好:展示有帮助的错误信息,而非“API Error 500”
❌ 无重试逻辑:API不可靠,需始终实现带退避的重试
❌ 缺少签名验证:攻击者可以伪造Webhook
Summary
总结
Great API integrations:
- ✅ Handle auth flows properly (OAuth, refresh tokens)
- ✅ Respect rate limits
- ✅ Retry transient failures with exponential backoff
- ✅ Verify webhook signatures
- ✅ Process webhooks idempotently
- ✅ Monitor health and error rates
- ✅ Show clear status to users
- ✅ Store credentials securely
优秀的API集成具备以下特点:
- ✅ 正确处理认证流程(OAuth、刷新令牌)
- ✅ 遵守速率限制
- ✅ 对临时故障执行带指数退避的重试
- ✅ 验证Webhook签名
- ✅ 幂等性处理Webhook
- ✅ 监控健康状态与错误率
- ✅ 向用户展示清晰的状态
- ✅ 安全存储凭证