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

  1. API Keys and Secrets

    • Store securely in environment variables
    • Never expose in client-side code
    • Rotate regularly
    • Use different keys for development/production
  2. Error Handling

    • Implement proper error handling
    • Log errors appropriately
    • Provide meaningful error messages
    • Handle rate limits and retries
  3. Data Validation

    • Validate input/output data
    • Use type-safe interfaces
    • Handle edge cases
    • Sanitize user input
  4. Performance

    • Implement caching where appropriate
    • Handle rate limits
    • Use connection pooling
    • Monitor API usage

Integration Checklist

  1. Setup

    • [ ] Configure API keys
    • [ ] Set up error tracking
    • [ ] Configure webhooks
    • [ ] Test endpoints
  2. Implementation

    • [ ] Add type definitions
    • [ ] Implement error handling
    • [ ] Add logging
    • [ ] Set up monitoring
  3. Testing

    • [ ] Test success cases
    • [ ] Test error cases
    • [ ] Test rate limits
    • [ ] Test webhooks
  4. Documentation

    • [ ] Document setup process
    • [ ] List configuration options
    • [ ] Provide usage examples
    • [ ] Include troubleshooting guide