Integrations Guide for integrating third-party services, APIs, payment processing, and email services in ShipKit applications
Integrations
This guide covers integrating external services and APIs into ShipKit applications, including third-party services, payment processing, and email services.
Third-Party Services
OpenAI Integration
// src/lib/integrations/openai.ts
import OpenAI from 'openai'
import { env } from '@/env.mjs'
const openai = new OpenAI({
apiKey: env.OPENAI_API_KEY,
})
export async function generateCompletion(prompt: string) {
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 500,
})
return completion.choices[0].message.content
} catch (error) {
console.error('OpenAI API error:', error)
throw new Error('Failed to generate completion')
}
}
export async function generateEmbedding(text: string) {
try {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
})
return response.data[0].embedding
} catch (error) {
console.error('OpenAI API error:', error)
throw new Error('Failed to generate embedding')
}
}
Vercel Blob Storage
// src/lib/integrations/storage.ts
import { put, del, list } from '@vercel/blob'
import { env } from '@/env.mjs'
export async function uploadFile(file: File, options?: { access?: 'public' | 'private' }) {
try {
const { url } = await put(file.name, file, {
access: options?.access || 'public',
addRandomSuffix: true,
})
return url
} catch (error) {
console.error('Blob storage error:', error)
throw new Error('Failed to upload file')
}
}
export async function deleteFile(url: string) {
try {
await del(url)
} catch (error) {
console.error('Blob storage error:', error)
throw new Error('Failed to delete file')
}
}
export async function listFiles(prefix?: string) {
try {
const { blobs } = await list({ prefix })
return blobs
} catch (error) {
console.error('Blob storage error:', error)
throw new Error('Failed to list files')
}
}
Upstash Redis
// src/lib/integrations/redis.ts
import { Redis } from '@upstash/redis'
import { env } from '@/env.mjs'
const redis = new Redis({
url: env.UPSTASH_REDIS_URL,
token: env.UPSTASH_REDIS_TOKEN,
})
export async function cacheData<T>(
key: string,
data: T,
expirationSeconds?: number
) {
try {
if (expirationSeconds) {
await redis.set(key, JSON.stringify(data), { ex: expirationSeconds })
} else {
await redis.set(key, JSON.stringify(data))
}
} catch (error) {
console.error('Redis error:', error)
throw new Error('Failed to cache data')
}
}
export async function getCachedData<T>(key: string): Promise<T | null> {
try {
const data = await redis.get(key)
return data ? JSON.parse(data as string) : null
} catch (error) {
console.error('Redis error:', error)
throw new Error('Failed to get cached data')
}
}
export async function invalidateCache(key: string) {
try {
await redis.del(key)
} catch (error) {
console.error('Redis error:', error)
throw new Error('Failed to invalidate cache')
}
}
Payment Processing
Stripe Integration
// src/lib/integrations/stripe.ts
import Stripe from 'stripe'
import { env } from '@/env.mjs'
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16',
typescript: true,
})
export async function createCheckoutSession({
priceId,
successUrl,
cancelUrl,
customerId,
}: {
priceId: string
successUrl: string
cancelUrl: string
customerId?: string
}) {
try {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: successUrl,
cancel_url: cancelUrl,
customer: customerId,
})
return session
} catch (error) {
console.error('Stripe error:', error)
throw new Error('Failed to create checkout session')
}
}
export async function createCustomer({
email,
name,
}: {
email: string
name: string
}) {
try {
const customer = await stripe.customers.create({
email,
name,
})
return customer
} catch (error) {
console.error('Stripe error:', error)
throw new Error('Failed to create customer')
}
}
export async function handleWebhook(
body: string,
signature: string
): Promise<{ type: string; data: any }> {
try {
const event = stripe.webhooks.constructEvent(
body,
signature,
env.STRIPE_WEBHOOK_SECRET
)
return {
type: event.type,
data: event.data.object,
}
} catch (error) {
console.error('Stripe webhook error:', error)
throw new Error('Failed to handle webhook')
}
}
Stripe Webhook Handler
// src/app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'
import { handleWebhook } from '@/lib/integrations/stripe'
import { db } from '@/server/db'
export async function POST(req: Request) {
try {
const body = await req.text()
const signature = headers().get('stripe-signature')
if (!signature) {
return new NextResponse('No signature', { status: 400 })
}
const event = await handleWebhook(body, signature)
// Handle different event types
switch (event.type) {
case 'customer.subscription.created':
await db.subscription.create({
data: {
stripeSubscriptionId: event.data.id,
userId: event.data.metadata.userId,
status: event.data.status,
priceId: event.data.items.data[0].price.id,
},
})
break
case 'customer.subscription.updated':
await db.subscription.update({
where: { stripeSubscriptionId: event.data.id },
data: { status: event.data.status },
})
break
case 'customer.subscription.deleted':
await db.subscription.update({
where: { stripeSubscriptionId: event.data.id },
data: { status: 'canceled' },
})
break
}
return new NextResponse(null, { status: 200 })
} catch (error) {
console.error('Webhook error:', error)
return new NextResponse('Webhook error', { status: 400 })
}
}
Email Services
Resend Integration
// src/lib/integrations/email.ts
import { Resend } from 'resend'
import { env } from '@/env.mjs'
const resend = new Resend(env.RESEND_API_KEY)
interface SendEmailOptions {
to: string
subject: string
react: React.ReactElement
}
export async function sendEmail({ to, subject, react }: SendEmailOptions) {
try {
const { data, error } = await resend.emails.send({
from: 'ShipKit <[email protected] >',
to,
subject,
react,
})
if (error) {
throw error
}
return data
} catch (error) {
console.error('Email error:', error)
throw new Error('Failed to send email')
}
}
export async function sendWelcomeEmail(to: string, name: string) {
return sendEmail({
to,
subject: 'Welcome to ShipKit!',
react: (
<WelcomeEmail
name={name}
loginUrl={`${env.NEXT_PUBLIC_APP_URL}/auth/signin`}
/>
),
})
}
export async function sendPasswordResetEmail(to: string, token: string) {
return sendEmail({
to,
subject: 'Reset Your Password',
react: (
<PasswordResetEmail
resetUrl={`${env.NEXT_PUBLIC_APP_URL}/auth/reset-password?token=${token}`}
/>
),
})
}
Email Templates
// src/components/emails/welcome-email.tsx
import {
Body,
Container,
Head,
Heading,
Html,
Link,
Preview,
Text,
} from '@react-email/components'
interface WelcomeEmailProps {
name: string
loginUrl: string
}
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to ShipKit</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Welcome to ShipKit!</Heading>
<Text style={text}>Hi {name},</Text>
<Text style={text}>
Thank you for signing up for ShipKit. We're excited to have you on
board!
</Text>
<Link href={loginUrl} style={button}>
Get Started
</Link>
</Container>
</Body>
</Html>
)
}
const main = {
backgroundColor: '#ffffff',
}
const container = {
margin: '0 auto',
padding: '20px 0 48px',
}
const h1 = {
color: '#1a1a1a',
fontSize: '24px',
fontWeight: '600',
lineHeight: '40px',
margin: '0 0 20px',
}
const text = {
color: '#1a1a1a',
fontSize: '16px',
lineHeight: '24px',
margin: '0 0 16px',
}
const button = {
backgroundColor: '#000000',
borderRadius: '4px',
color: '#ffffff',
display: 'inline-block',
fontSize: '16px',
padding: '12px 24px',
textDecoration: 'none',
}
API Integrations
API Client
// src/lib/integrations/api-client.ts
import { env } from '@/env.mjs'
interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
body?: any
headers?: Record<string, string>
}
export class ApiClient {
private baseUrl: string
private apiKey: string
constructor(baseUrl: string, apiKey: string) {
this.baseUrl = baseUrl
this.apiKey = apiKey
}
async request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
const url = `${this.baseUrl}${endpoint}`
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
...options.headers,
}
try {
const response = await fetch(url, {
method: options.method || 'GET',
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
})
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`)
}
return response.json()
} catch (error) {
console.error('API request error:', error)
throw new Error('Failed to make API request')
}
}
}
// Example usage
export const githubClient = new ApiClient(
'https://api.github.com',
env.GITHUB_API_KEY
)
export const slackClient = new ApiClient(
'https://slack.com/api',
env.SLACK_API_TOKEN
)
Integration Best Practices
API Keys and Secrets
Store securely in environment variables
Never expose in client-side code
Rotate regularly
Use different keys for development/production
Error Handling
Implement proper error handling
Log errors appropriately
Provide meaningful error messages
Handle rate limits and retries
Data Validation
Validate input/output data
Use type-safe interfaces
Handle edge cases
Sanitize user input
Performance
Implement caching where appropriate
Handle rate limits
Use connection pooling
Monitor API usage
Integration Checklist
Setup
[ ] Configure API keys
[ ] Set up error tracking
[ ] Configure webhooks
[ ] Test endpoints
Implementation
[ ] Add type definitions
[ ] Implement error handling
[ ] Add logging
[ ] Set up monitoring
Testing
[ ] Test success cases
[ ] Test error cases
[ ] Test rate limits
[ ] Test webhooks
Documentation
[ ] Document setup process
[ ] List configuration options
[ ] Provide usage examples
[ ] Include troubleshooting guide