Skip to content

Design Patterns

Common patterns used in OpenPets development.

Plugin Patterns

Basic Plugin Structure

typescript
import { z, createPlugin, createLogger, loadEnv, type ToolDefinition } from "openpets-sdk"

export const MyPet = async () => {
  const logger = createLogger("my-pet")
  const env = loadEnv("my-pet")

  // Check configuration
  if (!env.API_KEY) {
    logger.warn("Not configured")
    return createPlugin([testConnectionTool])
  }

  // Create tools
  const tools: ToolDefinition[] = [
    // ... tool definitions
  ]

  return createPlugin(tools)
}

export default MyPet

Multi-Provider Pattern

Support multiple backends for the same functionality:

typescript
type Provider = "google" | "mapbox" | "osm"

const getProvider = (env: Record<string, string | undefined>): Provider => {
  if (env.GOOGLE_MAPS_KEY) return "google"
  if (env.MAPBOX_TOKEN) return "mapbox"
  return "osm"
}

export const MapsPet = async () => {
  const env = loadEnv("maps")
  const provider = getProvider(env)
  
  const geocode = async (address: string) => {
    switch (provider) {
      case "google": return googleGeocode(address)
      case "mapbox": return mapboxGeocode(address)
      default: return osmGeocode(address)
    }
  }
  
  // Use geocode in tools...
}

API Client Pattern

Centralize API interactions:

typescript
const createApiClient = (baseUrl: string, apiKey: string) => {
  const request = async (endpoint: string, options: RequestInit = {}) => {
    const response = await fetch(`${baseUrl}${endpoint}`, {
      ...options,
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        ...options.headers
      }
    })
    
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`)
    }
    
    return response.json()
  }
  
  return {
    get: (endpoint: string) => request(endpoint),
    post: (endpoint: string, data: any) => 
      request(endpoint, { method: "POST", body: JSON.stringify(data) }),
    put: (endpoint: string, data: any) => 
      request(endpoint, { method: "PUT", body: JSON.stringify(data) }),
    delete: (endpoint: string) => 
      request(endpoint, { method: "DELETE" })
  }
}

Tool Patterns

CRUD Tool Set

Standard operations for a resource:

typescript
const createCrudTools = (resource: string, api: ApiClient): ToolDefinition[] => [
  {
    name: `my-pet-list-${resource}s`,
    description: `List all ${resource}s`,
    schema: z.object({
      limit: z.number().optional(),
      offset: z.number().optional()
    }),
    execute: async (args) => {
      const data = await api.get(`/${resource}s?limit=${args.limit || 50}`)
      return JSON.stringify(data, null, 2)
    }
  },
  {
    name: `my-pet-get-${resource}`,
    description: `Get a specific ${resource}`,
    schema: z.object({ id: z.string() }),
    execute: async (args) => {
      const data = await api.get(`/${resource}s/${args.id}`)
      return JSON.stringify(data, null, 2)
    }
  },
  {
    name: `my-pet-create-${resource}`,
    description: `Create a new ${resource}`,
    schema: z.object({ name: z.string(), description: z.string().optional() }),
    execute: async (args) => {
      const data = await api.post(`/${resource}s`, args)
      return JSON.stringify(data, null, 2)
    }
  },
  // ... update, delete
]

Pagination Pattern

Handle paginated APIs:

typescript
{
  name: "my-pet-list-items",
  schema: z.object({
    page: z.number().optional().describe("Page number (default: 1)"),
    perPage: z.number().optional().describe("Items per page (default: 50)")
  }),
  execute: async (args) => {
    const page = args.page || 1
    const perPage = args.perPage || 50
    
    const data = await api.get(`/items?page=${page}&per_page=${perPage}`)
    
    return JSON.stringify({
      items: data.items,
      pagination: {
        page,
        perPage,
        total: data.total,
        totalPages: Math.ceil(data.total / perPage)
      }
    }, null, 2)
  }
}

Search Pattern

Flexible search with filters:

typescript
{
  name: "my-pet-search",
  schema: z.object({
    query: z.string().describe("Search query"),
    status: z.enum(["active", "archived", "all"]).optional(),
    sortBy: z.enum(["created", "updated", "name"]).optional(),
    sortOrder: z.enum(["asc", "desc"]).optional()
  }),
  execute: async (args) => {
    const params = new URLSearchParams({ q: args.query })
    if (args.status) params.append("status", args.status)
    if (args.sortBy) params.append("sort", args.sortBy)
    if (args.sortOrder) params.append("order", args.sortOrder)
    
    const data = await api.get(`/search?${params}`)
    return JSON.stringify(data, null, 2)
  }
}

Error Handling Patterns

Structured Error Response

typescript
const handleError = (error: unknown, context: Record<string, any> = {}) => {
  const message = error instanceof Error ? error.message : "Unknown error"
  return JSON.stringify({
    success: false,
    error: message,
    ...context
  }, null, 2)
}

// Usage in execute:
execute: async (args) => {
  try {
    const data = await api.get(`/items/${args.id}`)
    return JSON.stringify({ success: true, data }, null, 2)
  } catch (error) {
    return handleError(error, { itemId: args.id })
  }
}

Retry Pattern

typescript
const withRetry = async <T>(
  fn: () => Promise<T>,
  retries = 3,
  delay = 1000
): Promise<T> => {
  try {
    return await fn()
  } catch (error) {
    if (retries > 0) {
      await new Promise(r => setTimeout(r, delay))
      return withRetry(fn, retries - 1, delay * 2)
    }
    throw error
  }
}

Caching Patterns

Response Caching

typescript
import { LocalCache } from "openpets-sdk"

const cache = new LocalCache<any>({ ttl: 60000 })

{
  name: "my-pet-get-item",
  execute: async (args) => {
    const cacheKey = `item:${args.id}`
    
    const cached = cache.get(cacheKey)
    if (cached) return cached
    
    const data = await api.get(`/items/${args.id}`)
    const result = JSON.stringify(data, null, 2)
    
    cache.set(cacheKey, result)
    return result
  }
}

Next Steps