service-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseResources
相关资源
scripts/
validate-services.sh
references/
service-patterns.mdscripts/
validate-services.sh
references/
service-patterns.mdService Integration Implementation
服务集成实现指南
This skill guides you through integrating external services into applications, from service selection to production deployment. It leverages GoodVibes precision tools for type-safe, resilient service integrations with proper error handling and testing.
本技能将指导你完成外部服务到应用的集成流程,从服务选型到生产环境部署。它借助GoodVibes精准工具实现类型安全、具备弹性的服务集成,并包含完善的错误处理与测试方案。
When to Use This Skill
适用场景
Use this skill when you need to:
- Integrate email providers (Resend, SendGrid, Postmark)
- Set up content management systems (Sanity, Contentful, Payload, Strapi)
- Implement file upload services (UploadThing, Cloudinary, S3)
- Add analytics and tracking (PostHog, Plausible, Google Analytics)
- Configure webhook endpoints for third-party services
- Implement retry logic and circuit breakers
- Test service integrations without hitting production APIs
当你需要以下操作时,可使用本技能:
- 集成邮件服务商(Resend、SendGrid、Postmark)
- 搭建内容管理系统(Sanity、Contentful、Payload、Strapi)
- 实现文件上传服务(UploadThing、Cloudinary、S3)
- 添加分析与追踪功能(PostHog、Plausible、Google Analytics)
- 配置第三方服务的Webhook端点
- 实现重试逻辑与断路器机制
- 在不调用生产API的情况下测试服务集成
Prerequisites
前置条件
Required context:
- Service requirements (email, CMS, uploads, analytics)
- Expected traffic volume and scale requirements
- Budget constraints
- Self-hosted vs managed service preference
Tools:
- precision_grep for detecting existing integrations
- precision_read for analyzing service configurations
- precision_write for creating integration code
- precision_exec for testing and validation
- discover for batch analysis
必要上下文:
- 服务需求(邮件、CMS、上传、分析)
- 预期流量规模与性能要求
- 预算限制
- 自托管与托管服务的偏好
工具:
- precision_grep:检测现有集成
- precision_read:分析服务配置
- precision_write:创建集成代码
- precision_exec:测试与验证
- discover:批量分析
Phase 1: Discovery - Detect Existing Integrations
阶段1:发现 - 检测现有集成
Before adding new services, analyze existing integrations to maintain consistency.
在添加新服务前,先分析现有集成以保持一致性。
Step 1.1: Detect Service SDKs and Patterns
步骤1.1:检测服务SDK与模式
Use discover to analyze multiple aspects in parallel:
yaml
discover:
queries:
- id: email-sdks
type: grep
pattern: "(resend|sendgrid|postmark|nodemailer)"
glob: "package.json"
- id: cms-sdks
type: grep
pattern: "(@sanity|contentful|@payloadcms|@strapi)"
glob: "package.json"
- id: upload-sdks
type: grep
pattern: "(uploadthing|cloudinary|@aws-sdk/client-s3)"
glob: "package.json"
- id: analytics-sdks
type: grep
pattern: "(posthog-js|plausible|@vercel/analytics)"
glob: "package.json"
- id: api-keys-env
type: grep
pattern: "(RESEND_|SENDGRID_|SANITY_|CONTENTFUL_|UPLOADTHING_|CLOUDINARY_|AWS_|POSTHOG_)"
glob: ".env.example"
- id: service-clients
type: glob
patterns: ["src/lib/*client.ts", "src/services/**/*.ts", "lib/services/**/*.ts"]
- id: webhook-routes
type: glob
patterns: ["src/app/api/webhooks/**/*.ts", "pages/api/webhooks/**/*.ts"]
verbosity: files_only使用discover并行分析多个维度:
yaml
discover:
queries:
- id: email-sdks
type: grep
pattern: "(resend|sendgrid|postmark|nodemailer)"
glob: "package.json"
- id: cms-sdks
type: grep
pattern: "(@sanity|contentful|@payloadcms|@strapi)"
glob: "package.json"
- id: upload-sdks
type: grep
pattern: "(uploadthing|cloudinary|@aws-sdk/client-s3)"
glob: "package.json"
- id: analytics-sdks
type: grep
pattern: "(posthog-js|plausible|@vercel/analytics)"
glob: "package.json"
- id: api-keys-env
type: grep
pattern: "(RESEND_|SENDGRID_|SANITY_|CONTENTFUL_|UPLOADTHING_|CLOUDINARY_|AWS_|POSTHOG_)"
glob: ".env.example"
- id: service-clients
type: glob
patterns: ["src/lib/*client.ts", "src/services/**/*.ts", "lib/services/**/*.ts"]
- id: webhook-routes
type: glob
patterns: ["src/app/api/webhooks/**/*.ts", "pages/api/webhooks/**/*.ts"]
verbosity: files_onlyStep 1.2: Analyze Service Client Patterns
步骤1.2:分析服务客户端模式
Read existing service clients to understand the project's patterns:
yaml
precision_read:
files:
- path: "src/lib/email-client.ts"
extract: outline
- path: "src/services/upload.ts"
extract: symbols
output:
format: minimalDecision Point: Use existing patterns for new integrations. If no patterns exist, follow the implementation guide below.
读取现有服务客户端以了解项目的模式:
yaml
precision_read:
files:
- path: "src/lib/email-client.ts"
extract: outline
- path: "src/services/upload.ts"
extract: symbols
output:
format: minimal决策点: 新集成沿用现有模式。若不存在现有模式,请遵循下方的实现指南。
Phase 2: Service Selection
阶段2:服务选型
Choose services based on requirements, scale, and budget.
根据需求、规模与预算选择合适的服务。
Email Services Decision Tree
邮件服务决策树
For transactional emails:
- Resend - Best DX, React Email support, 100 emails/day free, $20/month for 50K
- SendGrid - Enterprise features, 100 emails/day free, $20/month for 100K
- Postmark - High deliverability focus, $15/month for 10K, no free tier
- AWS SES - Cheapest at scale ($0.10/1000), requires more setup
For marketing emails:
- ConvertKit - Creator-focused, $29/month for 1K subscribers
- Mailchimp - All-in-one platform, free for 500 subscribers
- Loops - Developer-friendly, $49/month for 2K subscribers
Recommendation: Start with Resend for transactional, migrate to SES at 1M+ emails/month.
事务性邮件:
- Resend - 开发体验最佳,支持React Email,每日免费100封,5万封/月仅需20美元
- SendGrid - 企业级功能,每日免费100封,10万封/月仅需20美元
- Postmark - 专注高送达率,1万封/月15美元,无免费版
- AWS SES - 大规模场景下成本最低(每1000封0.1美元),但配置更复杂
营销邮件:
- ConvertKit - 面向创作者,1000订阅者/月29美元
- Mailchimp - 一体化平台,500订阅者以内免费
- Loops - 开发者友好,2000订阅者/月49美元
推荐: 事务性邮件先从Resend开始,月发送量达100万+时迁移至SES。
CMS Platform Decision Tree
CMS平台决策树
For structured content (blog, docs):
- Sanity - Best DX, real-time collaboration, free tier generous
- Contentful - Enterprise-ready, robust GraphQL, complex pricing
- Payload - Self-hosted, full TypeScript, no vendor lock-in
For app content (products, user-generated):
- Payload - Best for complex data models, authentication built-in
- Strapi - Large plugin ecosystem, self-hosted
For marketing pages:
- Builder.io - Visual editor, A/B testing built-in
- Sanity - Developer-friendly, visual editing with Sanity Studio
Recommendation: Sanity for content-heavy sites, Payload for app backends.
结构化内容(博客、文档):
- Sanity - 开发体验最佳,支持实时协作,免费版额度充足
- Contentful - 企业级就绪,GraphQL功能强大,定价复杂
- Payload - 自托管,全TypeScript支持,无供应商锁定
应用内容(产品、用户生成内容):
- Payload - 复杂数据模型的最佳选择,内置认证功能
- Strapi - 庞大的插件生态,支持自托管
营销页面:
- Builder.io - 可视化编辑器,内置A/B测试
- Sanity - 开发者友好,通过Sanity Studio支持可视化编辑
推荐: 内容密集型站点选Sanity,应用后端选Payload。
File Upload Decision Tree
文件上传决策树
For images (profile pics, product photos):
- UploadThing - Zero config, Next.js integration, $10/month for 2GB storage
- Cloudinary - Image transformations, free tier 25GB bandwidth
- Vercel Blob - Edge network, $0.15/GB storage
For large files (videos, documents):
- AWS S3 - Industry standard, $0.023/GB storage, cheapest at scale
- Cloudflare R2 - S3-compatible, zero egress fees, $0.015/GB storage
- Backblaze B2 - Cheapest storage at $0.005/GB
For user-facing uploads with virus scanning:
- UploadThing - Built-in virus scanning
- AWS S3 + Lambda - DIY scanning with ClamAV
Recommendation: UploadThing for prototypes, S3/R2 for production scale.
图片(头像、产品照片):
- UploadThing - 零配置,支持Next.js集成,2GB存储/月10美元
- Cloudinary - 支持图片转换,免费版提供25GB带宽
- Vercel Blob - 边缘网络,每GB存储0.15美元
大文件(视频、文档):
- AWS S3 - 行业标准,每GB存储0.023美元,大规模场景下成本最低
- Cloudflare R2 - 兼容S3,无出口流量费,每GB存储0.015美元
- Backblaze B2 - 存储成本最低,每GB0.005美元
带病毒扫描的用户端上传:
- UploadThing - 内置病毒扫描
- AWS S3 + Lambda - 结合ClamAV自行搭建扫描功能
推荐: 原型开发选UploadThing,生产大规模场景选S3/R2。
Analytics Decision Tree
分析服务决策树
For product analytics:
- PostHog - Self-hosted option, session replay, feature flags, free tier 1M events
- Mixpanel - User-centric analytics, free tier 20M events/month
- Amplitude - Advanced cohort analysis, free tier 10M events/month
For web analytics:
- Plausible - Privacy-focused, GDPR-compliant, $9/month for 10K pageviews
- Umami - Self-hosted, simple, open source
- Vercel Analytics - Web Vitals focus, free for Vercel projects
Recommendation: PostHog for product apps, Plausible for marketing sites.
产品分析:
- PostHog - 支持自托管,会话重放、功能标志,免费版每月100万事件
- Mixpanel - 以用户为中心的分析,免费版每月2000万事件
- Amplitude - 高级群组分析,免费版每月1000万事件
网站分析:
- Plausible - 隐私优先,符合GDPR,1万页面浏览量/月9美元
- Umami - 自托管,简洁开源
- Vercel Analytics - 聚焦Web Vitals,Vercel项目免费使用
推荐: 产品应用选PostHog,营销站点选Plausible。
Phase 3: Email Integration
阶段3:邮件集成
Step 3.1: Install Resend SDK
步骤3.1:安装Resend SDK
yaml
precision_exec:
commands:
- cmd: "npm install resend"
expect:
exit_code: 0
verbosity: minimalyaml
precision_exec:
commands:
- cmd: "npm install resend"
expect:
exit_code: 0
verbosity: minimalStep 3.2: Create Email Client
步骤3.2:创建邮件客户端
Write a type-safe email client with error handling:
yaml
precision_write:
files:
- path: "src/lib/email.ts"
mode: fail_if_exists
content: |
import type { ReactElement } from 'react';
import { Resend } from 'resend';
if (!process.env.RESEND_API_KEY) {
throw new Error('RESEND_API_KEY is required');
}
export const resend = new Resend(process.env.RESEND_API_KEY);
export interface SendEmailOptions {
to: string | string[];
subject: string;
html?: string;
react?: ReactElement;
from?: string;
}
export async function sendEmail(options: SendEmailOptions) {
const { to, subject, html, react, from = 'noreply@yourapp.com' } = options;
try {
const { data, error } = await resend.emails.send({
from,
to,
subject,
html,
react,
});
if (error) {
console.error('[Email] Send failed:', error);
throw new Error(`Email send failed: ${error.message}`);
}
console.log('[Email] Sent successfully:', data?.id);
return { success: true, id: data?.id };
} catch (error: unknown) {
console.error('[Email] Unexpected error:', error);
throw error;
}
}
verbosity: minimal编写带错误处理的类型安全邮件客户端:
yaml
precision_write:
files:
- path: "src/lib/email.ts"
mode: fail_if_exists
content: |
import type { ReactElement } from 'react';
import { Resend } from 'resend';
if (!process.env.RESEND_API_KEY) {
throw new Error('RESEND_API_KEY is required');
}
export const resend = new Resend(process.env.RESEND_API_KEY);
export interface SendEmailOptions {
to: string | string[];
subject: string;
html?: string;
react?: ReactElement;
from?: string;
}
export async function sendEmail(options: SendEmailOptions) {
const { to, subject, html, react, from = 'noreply@yourapp.com' } = options;
try {
const { data, error } = await resend.emails.send({
from,
to,
subject,
html,
react,
});
if (error) {
console.error('[Email] Send failed:', error);
throw new Error(`Email send failed: ${error.message}`);
}
console.log('[Email] Sent successfully:', data?.id);
return { success: true, id: data?.id };
} catch (error: unknown) {
console.error('[Email] Unexpected error:', error);
throw error;
}
}
verbosity: minimalStep 3.3: Create React Email Templates
步骤3.3:创建React Email模板
For transactional emails, use React Email for type-safe templates:
yaml
precision_exec:
commands:
- cmd: "npm install react-email @react-email/components"
expect:
exit_code: 0
verbosity: minimalyaml
precision_write:
files:
- path: "emails/welcome.tsx"
mode: fail_if_exists
content: |
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from '@react-email/components';
interface WelcomeEmailProps {
userName: string;
loginUrl: string;
}
export default function WelcomeEmail({ userName, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to our platform!</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Welcome, {userName}!</Heading>
<Text style={text}>
We're excited to have you on board. Click the button below to get started.
</Text>
<Button href={loginUrl} style={button}>
Get Started
</Button>
</Container>
</Body>
</Html>
);
}
const main = { backgroundColor: '#f6f9fc', fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif' };
const container = { margin: '0 auto', padding: '40px 20px' };
const h1 = { color: '#1f2937', fontSize: '24px', fontWeight: 'bold', marginBottom: '20px' };
const text = { color: '#4b5563', fontSize: '16px', lineHeight: '24px', marginBottom: '20px' };
const button = { backgroundColor: '#3b82f6', borderRadius: '6px', color: '#ffffff', display: 'inline-block', fontSize: '16px', fontWeight: '600', padding: '12px 24px', textDecoration: 'none' };
verbosity: minimal事务性邮件使用React Email实现类型安全模板:
yaml
precision_exec:
commands:
- cmd: "npm install react-email @react-email/components"
expect:
exit_code: 0
verbosity: minimalyaml
precision_write:
files:
- path: "emails/welcome.tsx"
mode: fail_if_exists
content: |
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from '@react-email/components';
interface WelcomeEmailProps {
userName: string;
loginUrl: string;
}
export default function WelcomeEmail({ userName, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to our platform!</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Welcome, {userName}!</Heading>
<Text style={text}>
We're excited to have you on board. Click the button below to get started.
</Text>
<Button href={loginUrl} style={button}>
Get Started
</Button>
</Container>
</Body>
</Html>
);
}
const main = { backgroundColor: '#f6f9fc', fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif' };
const container = { margin: '0 auto', padding: '40px 20px' };
const h1 = { color: '#1f2937', fontSize: '24px', fontWeight: 'bold', marginBottom: '20px' };
const text = { color: '#4b5563', fontSize: '16px', lineHeight: '24px', marginBottom: '20px' };
const button = { backgroundColor: '#3b82f6', borderRadius: '6px', color: '#ffffff', display: 'inline-block', fontSize: '16px', fontWeight: '600', padding: '12px 24px', textDecoration: 'none' };
verbosity: minimalStep 3.4: Document Environment Variables
步骤3.4:记录环境变量
yaml
precision_write:
files:
- path: ".env.example"
mode: overwrite
content: |
# Email (Resend)
RESEND_API_KEY=your_resend_api_key_here
verbosity: minimalyaml
precision_write:
files:
- path: ".env.example"
mode: overwrite
content: |
# Email (Resend)
RESEND_API_KEY=your_resend_api_key_here
verbosity: minimalPhase 4: CMS Integration
阶段4:CMS集成
Step 4.1: Install Sanity SDK
步骤4.1:安装Sanity SDK
yaml
precision_exec:
commands:
- cmd: "npm install @sanity/client @sanity/image-url"
expect:
exit_code: 0
verbosity: minimalyaml
precision_exec:
commands:
- cmd: "npm install @sanity/client @sanity/image-url"
expect:
exit_code: 0
verbosity: minimalStep 4.2: Create Sanity Client
步骤4.2:创建Sanity客户端
yaml
precision_write:
files:
- path: "src/lib/sanity.ts"
mode: fail_if_exists
content: |
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
import type { SanityImageSource } from '@sanity/image-url/lib/types/types';
if (!process.env.NEXT_PUBLIC_SANITY_PROJECT_ID) {
throw new Error('NEXT_PUBLIC_SANITY_PROJECT_ID is required');
}
if (!process.env.NEXT_PUBLIC_SANITY_DATASET) {
throw new Error('NEXT_PUBLIC_SANITY_DATASET is required');
}
export const sanityClient = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2026-01-01', // Update to current API version date
useCdn: process.env.NODE_ENV === 'production',
});
const builder = imageUrlBuilder(sanityClient);
// Import from @sanity/image-url
export function urlForImage(source: SanityImageSource) {
return builder.image(source).auto('format').fit('max');
}
// Type-safe query helper
export async function sanityFetch<T>(query: string, params?: Record<string, unknown>): Promise<T> {
try {
const result = await sanityClient.fetch<T>(query, params);
return result;
} catch (error: unknown) {
console.error('[Sanity] Query failed:', error);
throw new Error('Failed to fetch from Sanity');
}
}
verbosity: minimalyaml
precision_write:
files:
- path: "src/lib/sanity.ts"
mode: fail_if_exists
content: |
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
import type { SanityImageSource } from '@sanity/image-url/lib/types/types';
if (!process.env.NEXT_PUBLIC_SANITY_PROJECT_ID) {
throw new Error('NEXT_PUBLIC_SANITY_PROJECT_ID is required');
}
if (!process.env.NEXT_PUBLIC_SANITY_DATASET) {
throw new Error('NEXT_PUBLIC_SANITY_DATASET is required');
}
export const sanityClient = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2026-01-01', // Update to current API version date
useCdn: process.env.NODE_ENV === 'production',
});
const builder = imageUrlBuilder(sanityClient);
// Import from @sanity/image-url
export function urlForImage(source: SanityImageSource) {
return builder.image(source).auto('format').fit('max');
}
// Type-safe query helper
export async function sanityFetch<T>(query: string, params?: Record<string, unknown>): Promise<T> {
try {
const result = await sanityClient.fetch<T>(query, params);
return result;
} catch (error: unknown) {
console.error('[Sanity] Query failed:', error);
throw new Error('Failed to fetch from Sanity');
}
}
verbosity: minimalStep 4.3: Set Up Webhook Endpoint
步骤4.3:配置Webhook端点
For real-time content updates, implement a webhook handler:
yaml
precision_write:
files:
- path: "src/app/api/webhooks/sanity/route.ts"
mode: fail_if_exists
content: |
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
import { timingSafeEqual } from 'crypto';
export async function POST(request: NextRequest) {
const signature = request.headers.get('sanity-webhook-signature');
// Verify webhook signature
const secret = process.env.SANITY_WEBHOOK_SECRET;
if (!secret) {
return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
}
const expected = Buffer.from(secret);
const received = Buffer.from(signature || '');
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
try {
const body = await request.json();
const { _type } = body;
// Revalidate cache based on content type
if (_type === 'post') {
revalidateTag('posts');
} else if (_type === 'page') {
revalidateTag('pages');
}
console.log('[Webhook] Sanity content updated:', _type); // Note: Use structured logger in production
return NextResponse.json({ revalidated: true });
} catch (error: unknown) {
console.error('[Webhook] Failed to process Sanity webhook:', error);
return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
}
}
verbosity: minimal为实现实时内容更新,编写Webhook处理器:
yaml
precision_write:
files:
- path: "src/app/api/webhooks/sanity/route.ts"
mode: fail_if_exists
content: |
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
import { timingSafeEqual } from 'crypto';
export async function POST(request: NextRequest) {
const signature = request.headers.get('sanity-webhook-signature');
// Verify webhook signature
const secret = process.env.SANITY_WEBHOOK_SECRET;
if (!secret) {
return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
}
const expected = Buffer.from(secret);
const received = Buffer.from(signature || '');
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
try {
const body = await request.json();
const { _type } = body;
// Revalidate cache based on content type
if (_type === 'post') {
revalidateTag('posts');
} else if (_type === 'page') {
revalidateTag('pages');
}
console.log('[Webhook] Sanity content updated:', _type); // Note: Use structured logger in production
return NextResponse.json({ revalidated: true });
} catch (error: unknown) {
console.error('[Webhook] Failed to process Sanity webhook:', error);
return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
}
}
verbosity: minimalPhase 5: File Upload Integration
阶段5:文件上传集成
Step 5.1: Install UploadThing SDK
步骤5.1:安装UploadThing SDK
yaml
precision_exec:
commands:
- cmd: "npm install uploadthing @uploadthing/react"
expect:
exit_code: 0
verbosity: minimalyaml
precision_exec:
commands:
- cmd: "npm install uploadthing @uploadthing/react"
expect:
exit_code: 0
verbosity: minimalStep 5.2: Create Upload Router
步骤5.2:创建上传路由
yaml
precision_write:
files:
- path: "src/app/api/uploadthing/core.ts"
mode: fail_if_exists
content: |
import { createUploadthing, type FileRouter } from 'uploadthing/next';
const f = createUploadthing();
export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: '4MB', maxFileCount: 4 } })
.middleware(async ({ req }) => {
// Authenticate user (placeholder imports shown for context)
// In real code: import { getUserFromRequest } from '@/lib/auth';
const user = await getUserFromRequest(req); // Import from your auth module
if (!user) throw new Error('Unauthorized');
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log('[Upload] Complete:', file.url);
// Save to database (Assumes Prisma client or similar ORM)
await db.image.create({
data: {
url: file.url,
userId: metadata.userId,
},
});
return { url: file.url };
}),
pdfUploader: f({ pdf: { maxFileSize: '16MB' } })
.middleware(async ({ req }) => {
const user = await getUserFromRequest(req);
if (!user) throw new Error('Unauthorized');
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log('[Upload] PDF complete:', file.url);
return { url: file.url };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;
verbosity: minimalyaml
precision_write:
files:
- path: "src/app/api/uploadthing/core.ts"
mode: fail_if_exists
content: |
import { createUploadthing, type FileRouter } from 'uploadthing/next';
const f = createUploadthing();
export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: '4MB', maxFileCount: 4 } })
.middleware(async ({ req }) => {
// Authenticate user (placeholder imports shown for context)
// In real code: import { getUserFromRequest } from '@/lib/auth';
const user = await getUserFromRequest(req); // Import from your auth module
if (!user) throw new Error('Unauthorized');
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log('[Upload] Complete:', file.url);
// Save to database (Assumes Prisma client or similar ORM)
await db.image.create({
data: {
url: file.url,
userId: metadata.userId,
},
});
return { url: file.url };
}),
pdfUploader: f({ pdf: { maxFileSize: '16MB' } })
.middleware(async ({ req }) => {
const user = await getUserFromRequest(req);
if (!user) throw new Error('Unauthorized');
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log('[Upload] PDF complete:', file.url);
return { url: file.url };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;
verbosity: minimalStep 5.3: Alternative - S3 Presigned URLs
步骤5.3:替代方案 - S3预签名URL
For more control, use S3 with presigned URLs:
yaml
precision_exec:
commands:
- cmd: "npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
expect:
exit_code: 0
verbosity: minimalyaml
precision_write:
files:
- path: "src/lib/s3.ts"
mode: fail_if_exists
content: |
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
// Validate S3 configuration
if (!process.env.AWS_REGION || !process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY || !process.env.AWS_S3_BUCKET) {
throw new Error('Missing required AWS S3 environment variables');
}
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
export async function getUploadUrl(key: string, contentType: string) {
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
ContentType: contentType,
});
// URL expires in 5 minutes
const url = await getSignedUrl(s3Client, command, { expiresIn: 300 });
return url;
}
verbosity: minimal如需更多控制权,使用S3预签名URL:
yaml
precision_exec:
commands:
- cmd: "npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
expect:
exit_code: 0
verbosity: minimalyaml
precision_write:
files:
- path: "src/lib/s3.ts"
mode: fail_if_exists
content: |
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
// Validate S3 configuration
if (!process.env.AWS_REGION || !process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY || !process.env.AWS_S3_BUCKET) {
throw new Error('Missing required AWS S3 environment variables');
}
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
export async function getUploadUrl(key: string, contentType: string) {
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
ContentType: contentType,
});
// URL expires in 5 minutes
const url = await getSignedUrl(s3Client, command, { expiresIn: 300 });
return url;
}
verbosity: minimalPhase 6: Analytics Integration
阶段6:分析集成
Step 6.1: Install PostHog SDK
步骤6.1:安装PostHog SDK
yaml
precision_exec:
commands:
- cmd: "npm install posthog-js"
expect:
exit_code: 0
verbosity: minimalyaml
precision_exec:
commands:
- cmd: "npm install posthog-js"
expect:
exit_code: 0
verbosity: minimalStep 6.2: Create Analytics Provider
步骤6.2:创建Analytics Provider
yaml
precision_write:
files:
- path: "src/providers/analytics.tsx"
mode: fail_if_exists
content: |
'use client';
import { useEffect } from 'react';
import type { ReactNode } from 'react';
import posthog from 'posthog-js';
export function AnalyticsProvider({ children }: { children: ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') {
posthog.opt_out_capturing(); // Disabled in dev to avoid polluting analytics (re-enable for testing with posthog.opt_in_capturing())
}
},
});
}
}, []);
return <>{children}</>;
}
// Helper for tracking events
export function trackEvent(eventName: string, properties?: Record<string, unknown>) {
if (typeof window !== 'undefined') {
posthog.capture(eventName, properties);
}
}
verbosity: minimalyaml
precision_write:
files:
- path: "src/providers/analytics.tsx"
mode: fail_if_exists
content: |
'use client';
import { useEffect } from 'react';
import type { ReactNode } from 'react';
import posthog from 'posthog-js';
export function AnalyticsProvider({ children }: { children: ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') {
posthog.opt_out_capturing(); // Disabled in dev to avoid polluting analytics (re-enable for testing with posthog.opt_in_capturing())
}
},
});
}
}, []);
return <>{children}</>;
}
// Helper for tracking events
export function trackEvent(eventName: string, properties?: Record<string, unknown>) {
if (typeof window !== 'undefined') {
posthog.capture(eventName, properties);
}
}
verbosity: minimalPhase 7: Error Handling Patterns
阶段7:错误处理模式
Step 7.1: Implement Exponential Backoff
步骤7.1:实现指数退避
For services with rate limits, implement retry logic:
yaml
precision_write:
files:
- path: "src/lib/retry.ts"
mode: fail_if_exists
content: |
export interface RetryOptions {
maxAttempts?: number;
initialDelayMs?: number;
maxDelayMs?: number;
backoffMultiplier?: number;
}
export async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxAttempts = 3,
initialDelayMs = 1000,
maxDelayMs = 10000,
backoffMultiplier = 2,
} = options;
let lastError: Error = new Error('All retry attempts failed');
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error: unknown) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === maxAttempts) {
break;
}
const delayMs = Math.min(
initialDelayMs * Math.pow(backoffMultiplier, attempt - 1),
maxDelayMs
);
console.log(`[Retry] Attempt ${attempt}/${maxAttempts} failed, retrying in ${delayMs}ms`); // Note: Use structured logger in production
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw lastError;
}
verbosity: minimal对有限流的服务,实现重试逻辑:
yaml
precision_write:
files:
- path: "src/lib/retry.ts"
mode: fail_if_exists
content: |
export interface RetryOptions {
maxAttempts?: number;
initialDelayMs?: number;
maxDelayMs?: number;
backoffMultiplier?: number;
}
export async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxAttempts = 3,
initialDelayMs = 1000,
maxDelayMs = 10000,
backoffMultiplier = 2,
} = options;
let lastError: Error = new Error('All retry attempts failed');
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error: unknown) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === maxAttempts) {
break;
}
const delayMs = Math.min(
initialDelayMs * Math.pow(backoffMultiplier, attempt - 1),
maxDelayMs
);
console.log(`[Retry] Attempt ${attempt}/${maxAttempts} failed, retrying in ${delayMs}ms`); // Note: Use structured logger in production
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw lastError;
}
verbosity: minimalStep 7.2: Implement Circuit Breaker
步骤7.2:实现断路器
Prevent cascading failures with a circuit breaker:
yaml
precision_write:
files:
- path: "src/lib/circuit-breaker.ts"
mode: fail_if_exists
content: |
export class CircuitBreaker {
private failureCount = 0;
private lastFailureTime: number | null = null;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private readonly failureThreshold = 5,
private readonly resetTimeoutMs = 60000
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - (this.lastFailureTime ?? 0) > this.resetTimeoutMs) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error: unknown) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = 'closed';
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.error('[CircuitBreaker] Circuit opened due to repeated failures');
}
}
}
verbosity: minimal通过断路器防止级联故障:
yaml
precision_write:
files:
- path: "src/lib/circuit-breaker.ts"
mode: fail_if_exists
content: |
export class CircuitBreaker {
private failureCount = 0;
private lastFailureTime: number | null = null;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private readonly failureThreshold = 5,
private readonly resetTimeoutMs = 60000
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - (this.lastFailureTime ?? 0) > this.resetTimeoutMs) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error: unknown) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = 'closed';
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.error('[CircuitBreaker] Circuit opened due to repeated failures');
}
}
}
verbosity: minimalPhase 8: Testing Service Integrations
阶段8:测试服务集成
Step 8.1: Mock Service Responses
步骤8.1:模拟服务响应
Create mock implementations for testing:
yaml
precision_write:
files:
- path: "src/lib/__mocks__/email.ts"
mode: fail_if_exists
content: |
import { SendEmailOptions } from '../email';
const sentEmails: Array<SendEmailOptions & { id: string }> = [];
export async function sendEmail(options: SendEmailOptions) {
const id = `mock-${Date.now()}`;
sentEmails.push({ ...options, id });
return { success: true, id };
}
export function getSentEmails() {
return sentEmails;
}
export function clearSentEmails() {
sentEmails.length = 0;
}
verbosity: minimal创建模拟实现用于测试:
yaml
precision_write:
files:
- path: "src/lib/__mocks__/email.ts"
mode: fail_if_exists
content: |
import { SendEmailOptions } from '../email';
const sentEmails: Array<SendEmailOptions & { id: string }> = [];
export async function sendEmail(options: SendEmailOptions) {
const id = `mock-${Date.now()}`;
sentEmails.push({ ...options, id });
return { success: true, id };
}
export function getSentEmails() {
return sentEmails;
}
export function clearSentEmails() {
sentEmails.length = 0;
}
verbosity: minimalStep 8.2: Implement Webhook Testing
步骤8.2:实现Webhook测试
Test webhook handlers locally:
yaml
precision_write:
files:
- path: "scripts/test-webhook.sh"
mode: fail_if_exists
content: |
#!/usr/bin/env bash
set -euo pipefail
# Requires: bash 4+
# Test Sanity webhook locally
echo "[INFO] Testing Sanity webhook..."
if curl -X POST http://localhost:3000/api/webhooks/sanity \
-H "Content-Type: application/json" \
-H "sanity-webhook-signature: $SANITY_WEBHOOK_SECRET" \
-d '{
"_type": "post",
"_id": "test-123",
"title": "Test Post"
}'; then
echo "[PASS] Webhook test successful"
else
echo "[FAIL] Webhook test failed"
exit 1
fi
verbosity: minimal本地测试Webhook处理器:
yaml
precision_write:
files:
- path: "scripts/test-webhook.sh"
mode: fail_if_exists
content: |
#!/usr/bin/env bash
set -euo pipefail
# Requires: bash 4+
# Test Sanity webhook locally
echo "[INFO] Testing Sanity webhook..."
if curl -X POST http://localhost:3000/api/webhooks/sanity \
-H "Content-Type: application/json" \
-H "sanity-webhook-signature: $SANITY_WEBHOOK_SECRET" \
-d '{
"_type": "post",
"_id": "test-123",
"title": "Test Post"
}'; then
echo "[PASS] Webhook test successful"
else
echo "[FAIL] Webhook test failed"
exit 1
fi
verbosity: minimalPhase 9: Validation
阶段9:验证
Run the validation script to ensure proper service integration:
yaml
precision_exec:
commands:
- cmd: "bash plugins/goodvibes/skills/outcome/service-integration/scripts/validate-services.sh ."
expect:
exit_code: 0
verbosity: standard运行验证脚本确保服务集成正确:
yaml
precision_exec:
commands:
- cmd: "bash plugins/goodvibes/skills/outcome/service-integration/scripts/validate-services.sh ."
expect:
exit_code: 0
verbosity: standardCommon Patterns
通用模式
Environment Variable Validation
环境变量验证
Always validate required environment variables at startup:
typescript
const requiredEnvVars = ['RESEND_API_KEY', 'SANITY_PROJECT_ID'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}启动时始终验证必要的环境变量:
typescript
const requiredEnvVars = ['RESEND_API_KEY', 'SANITY_PROJECT_ID'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}Rate Limiting Outbound Requests
出站请求限流
Implement token bucket rate limiting:
typescript
export class RateLimiter {
private tokens: number;
private lastRefill: number;
constructor(
private readonly maxTokens: number,
private readonly refillRatePerSecond: number
) {
this.tokens = maxTokens;
this.lastRefill = Date.now();
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens < 1) {
const waitMs = (1 - this.tokens) * (1000 / this.refillRatePerSecond);
await new Promise(resolve => setTimeout(resolve, waitMs));
this.refill();
}
this.tokens -= 1;
}
private refill() {
const now = Date.now();
const elapsedSeconds = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsedSeconds * this.refillRatePerSecond;
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}实现令牌桶限流:
typescript
export class RateLimiter {
private tokens: number;
private lastRefill: number;
constructor(
private readonly maxTokens: number,
private readonly refillRatePerSecond: number
) {
this.tokens = maxTokens;
this.lastRefill = Date.now();
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens < 1) {
const waitMs = (1 - this.tokens) * (1000 / this.refillRatePerSecond);
await new Promise(resolve => setTimeout(resolve, waitMs));
this.refill();
}
this.tokens -= 1;
}
private refill() {
const now = Date.now();
const elapsedSeconds = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsedSeconds * this.refillRatePerSecond;
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}Anti-Patterns to Avoid
需避免的反模式
1. Hardcoded API Keys
1. 硬编码API密钥
BAD:
typescript
const resend = new Resend('re_abc123');GOOD:
typescript
if (!process.env.RESEND_API_KEY) {
throw new Error('RESEND_API_KEY is required');
}
const resend = new Resend(process.env.RESEND_API_KEY);错误示例:
typescript
const resend = new Resend('re_abc123');正确示例:
typescript
if (!process.env.RESEND_API_KEY) {
throw new Error('RESEND_API_KEY is required');
}
const resend = new Resend(process.env.RESEND_API_KEY);2. No Error Handling
2. 无错误处理
BAD:
typescript
const result = await resend.emails.send(options);
return result.data;GOOD:
typescript
const { data, error } = await resend.emails.send(options);
if (error) {
console.error('[Email] Send failed:', error);
throw new Error(`Email send failed: ${error.message}`);
}
return data;错误示例:
typescript
const result = await resend.emails.send(options);
return result.data;正确示例:
typescript
const { data, error } = await resend.emails.send(options);
if (error) {
console.error('[Email] Send failed:', error);
throw new Error(`Email send failed: ${error.message}`);
}
return data;3. Synchronous External Calls
3. 同步外部调用
BAD:
typescript
// Blocking user request
await sendEmail({ to: user.email, subject: 'Welcome' });
return res.json({ success: true });ACCEPTABLE (for simple cases):
typescript
// Send email async
// Note: Fire-and-forget is only suitable for non-critical operations.
// For production: use a queue-based approach with retries (see BEST example below).
sendEmail({ to: user.email, subject: 'Welcome' })
.catch(err => console.error('[Email] Failed:', err));
return res.json({ success: true });BEST (production-ready):
typescript
// Queue-based approach with retries
await emailQueue.add('welcome-email', {
to: user.email,
subject: 'Welcome'
});
return res.json({ success: true });错误示例:
typescript
// Blocking user request
await sendEmail({ to: user.email, subject: 'Welcome' });
return res.json({ success: true });可接受(简单场景):
typescript
// Send email async
// Note: Fire-and-forget is only suitable for non-critical operations.
// For production: use a queue-based approach with retries (see BEST example below).
sendEmail({ to: user.email, subject: 'Welcome' })
.catch(err => console.error('[Email] Failed:', err));
return res.json({ success: true });最佳实践(生产就绪):
typescript
// Queue-based approach with retries
await emailQueue.add('welcome-email', {
to: user.email,
subject: 'Welcome'
});
return res.json({ success: true });4. Missing Webhook Verification
4. 缺少Webhook验证
BAD:
typescript
export async function POST(request: NextRequest) {
const body = await request.json();
// Process without verification
}GOOD:
typescript
import { timingSafeEqual } from 'crypto';
export async function POST(request: NextRequest) {
const signature = request.headers.get('webhook-signature');
// Validate webhook secret
const secret = process.env.WEBHOOK_SECRET;
if (!secret) {
return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
}
const expected = Buffer.from(secret);
const received = Buffer.from(signature || '');
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const body = await request.json();
// Process
}错误示例:
typescript
export async function POST(request: NextRequest) {
const body = await request.json();
// Process without verification
}正确示例:
typescript
import { timingSafeEqual } from 'crypto';
export async function POST(request: NextRequest) {
const signature = request.headers.get('webhook-signature');
// Validate webhook secret
const secret = process.env.WEBHOOK_SECRET;
if (!secret) {
return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
}
const expected = Buffer.from(secret);
const received = Buffer.from(signature || '');
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const body = await request.json();
// Process
}Troubleshooting
故障排查
Email Not Sending
邮件无法发送
- Verify API key is set: (bash parameter expansion to display first 5 chars)
printf "%s\n" "${RESEND_API_KEY:0:5}..." - Check API key permissions in provider dashboard
- Verify sender domain is verified
- Check rate limits in provider logs
- Enable debug logging:
resend.setDebug(true)
- 验证API密钥已设置:(bash参数展开显示前5位)
printf "%s\n" "${RESEND_API_KEY:0:5}..." - 在服务商控制台检查API密钥权限
- 验证发件人域名已通过验证
- 在服务商日志中查看限流情况
- 启用调试日志:
resend.setDebug(true)
CMS Content Not Updating
CMS内容未更新
- Verify webhook endpoint is publicly accessible
- Check webhook secret matches
- Test webhook locally with ngrok
- Verify cache revalidation is working
- Check CMS webhook logs
- 验证Webhook端点可公开访问
- 检查Webhook密钥是否匹配
- 使用ngrok本地测试Webhook
- 验证缓存重新生效功能正常
- 查看CMS的Webhook日志
File Upload Failing
文件上传失败
- Verify file size is within limits
- Check CORS configuration
- Verify authentication middleware
- Test with smaller file
- Check S3 bucket permissions (if using S3)
- 验证文件大小在限制范围内
- 检查CORS配置
- 验证认证中间件
- 用更小的文件测试
- 检查S3桶权限(若使用S3)
Analytics Events Not Tracking
分析事件未追踪
- Verify API key is set
- Check ad blockers aren't blocking requests
- Verify opt-out is disabled in development
- Check browser console for errors
- Test with PostHog debug mode
- 验证API密钥已设置
- 检查广告拦截器是否阻止了请求
- 验证开发环境中未启用opt-out
- 查看浏览器控制台的错误信息
- 使用PostHog调试模式测试
Related Skills
相关技能
- api-design - Type-safe API layer patterns
- authentication - Secure service access patterns
- testing-strategy - Testing service integrations
- api-design - 类型安全API层模式
- authentication - 安全服务访问模式
- testing-strategy - 服务集成测试
Success Criteria
成功标准
- Service SDK installed and client created
- Environment variables documented in .env.example
- Error handling implemented with retries
- Webhook endpoints have signature verification
- Rate limiting configured for outbound requests
- Mock implementations for testing
- No hardcoded API keys in source code
- All validation checks pass
- 已安装服务SDK并创建客户端
- 环境变量已记录在.env.example中
- 已实现带重试的错误处理
- Webhook端点已添加签名验证
- 已配置出站请求限流
- 已创建测试用的模拟实现
- 源代码中无硬编码API密钥
- 所有验证检查已通过