Frontend

API Integration

Learn how Studio integrates with Cortex APIs using reactive composables and type-safe patterns.

Overview

Studio demonstrates best practices for integrating with Cortex APIs through reactive composables and type-safe patterns. This integration layer provides a foundation for building custom analytics interfaces.

Studio's API integration architecture and data flow

Composable Architecture

Studio uses Vue composables to encapsulate API communication, providing reactive state management and consistent error handling.

Base API Composable

The foundation for all API interactions in Studio.

Base API configuration and URL generation

Implementation:

// useApi.ts - Base API configuration
export function useApi() {
  const config = useRuntimeConfig()
  const apiBaseUrl = config.public.apiBaseUrl

  function apiUrl(path: string): string {
    return `${apiBaseUrl}${path}`
  }

  return {
    apiBaseUrl,
    apiUrl
  }
}

Features:

  • Environment Configuration: Runtime API base URL configuration
  • URL Generation: Consistent API URL construction
  • Configuration Access: Centralized runtime config access

Resource Composables

Specialized composables for different resource types.

How resource composables manage specific API endpoints

Key Composables:

Workspace Management

// useWorkspaces.ts
export function useWorkspaces() {
  const workspaces = ref<Workspace[]>([])
  const selectedWorkspaceId = ref<string | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function fetchWorkspaces() {
    loading.value = true
    try {
      const response = await $fetch<{workspaces: Workspace[]}>(
        apiUrl('/api/v1/workspaces')
      )
      workspaces.value = response.workspaces
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  return {
    workspaces: readonly(workspaces),
    selectedWorkspaceId,
    loading: readonly(loading),
    error: readonly(error),
    fetchWorkspaces,
    selectWorkspace
  }
}

Metric Management

// useMetrics.ts
export function useMetrics() {
  const metrics = ref<SemanticMetric[]>([])
  const currentMetric = ref<SemanticMetric | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function fetchMetrics(modelId: string, filters?: MetricFilters) {
    loading.value = true
    error.value = null
    
    try {
      let url = apiUrl(`/api/v1/data-models/${modelId}/metrics`)
      
      if (filters) {
        const params = new URLSearchParams()
        Object.entries(filters).forEach(([key, value]) => {
          if (value !== undefined) params.append(key, String(value))
        })
        if (params.toString()) url += `?${params.toString()}`
      }
      
      const response = await $fetch<{metrics: SemanticMetric[]}>(url)
      metrics.value = response.metrics
    } catch (err: any) {
      error.value = err.message || 'Failed to fetch metrics'
      throw err
    } finally {
      loading.value = false
    }
  }

  async function createMetric(metricData: CreateMetricRequest) {
    loading.value = true
    error.value = null
    
    try {
      const metric = await $fetch<SemanticMetric>(
        apiUrl('/api/v1/metrics'), 
        {
          method: 'POST',
          body: metricData
        }
      )
      
      metrics.value.push(metric)
      return metric
    } catch (err: any) {
      error.value = err.message || 'Failed to create metric'
      throw err
    } finally {
      loading.value = false
    }
  }

  return {
    metrics: readonly(metrics),
    currentMetric: readonly(currentMetric),
    loading: readonly(loading),
    error: readonly(error),
    fetchMetrics,
    createMetric,
    updateMetric,
    deleteMetric,
    executeMetric
  }
}

Dashboard Operations

// useDashboards.ts
export function useDashboards() {
  const dashboards = ref<Dashboard[]>([])
  const currentDashboard = ref<Dashboard | null>(null)
  
  async function executeDashboard(dashboardId: string, viewId?: string) {
    loading.value = true
    error.value = null
    
    try {
      let url = apiUrl(`/api/v1/dashboards/${dashboardId}/execute`)
      if (viewId) url += `?view_id=${viewId}`
      
      const result = await $fetch<DashboardExecutionResult>(url, {
        method: 'POST'
      })
      
      return result
    } catch (err: any) {
      error.value = err.message || 'Failed to execute dashboard'
      throw err
    } finally {
      loading.value = false
    }
  }

  async function previewDashboardConfig(dashboardId: string, config: any) {
    // Complex preview logic with config validation
    const previewConfig = {
      id: dashboardId,
      views: config.views?.map((view: any) => ({
        // Detailed view configuration mapping
      }))
    }
    
    const result = await $fetch<DashboardExecutionResult>(
      apiUrl(`/api/v1/dashboards/${dashboardId}/preview`),
      { method: 'POST', body: previewConfig }
    )
    
    return result
  }

  return {
    dashboards: readonly(dashboards),
    currentDashboard: readonly(currentDashboard),
    executeDashboard,
    previewDashboardConfig,
    // ... other methods
  }
}

Type Safety

Studio maintains strict type safety throughout the API integration layer.

TypeScript Interfaces

TypeScript interfaces for API requests and responses

Core Interfaces:

// Metric interfaces
export interface SemanticMetric {
  id: string
  name: string
  alias?: string
  title?: string
  description?: string
  data_model_id: string
  data_source_id?: string
  query?: string
  table_name?: string
  limit?: number
  grouped?: boolean
  parameters?: MetricParameter[]
  public: boolean
  measures?: any[]
  dimensions?: any[]
  joins?: any[]
  aggregations?: any[]
  filters?: any[]
  created_at: string
  updated_at: string
}

// Request interfaces
export interface CreateMetricRequest {
  name: string
  description?: string
  data_model_id: string
  query?: string
  table_name?: string
  measures?: any[]
  dimensions?: any[]
  // ... other fields
}

// Dashboard interfaces
export interface Dashboard {
  id: string
  name: string
  description?: string
  environment_id: string
  views: DashboardView[]
  default_view?: string
  tags?: string[]
  created_at: string
  updated_at: string
}

export interface DashboardView {
  alias: string
  title: string
  description?: string
  context_id?: string
  sections: DashboardSection[]
}

Runtime Validation

Runtime validation patterns for API responses

Validation Strategies:

// Zod schemas for runtime validation
const MetricSchema = z.object({
  id: z.string(),
  name: z.string(),
  data_model_id: z.string(),
  created_at: z.string(),
  // ... other fields
})

// Validation in composables
async function fetchMetric(metricId: string) {
  const response = await $fetch(apiUrl(`/api/v1/metrics/${metricId}`))
  
  // Validate response structure
  const validatedMetric = MetricSchema.parse(response)
  currentMetric.value = validatedMetric
  
  return validatedMetric
}

Error Handling

Consistent error handling patterns across all API interactions.

Error States

Error handling patterns and user feedback

Error Management:

export function useApiError() {
  const error = ref<string | null>(null)
  const isError = computed(() => !!error.value)

  function handleApiError(err: any, context: string) {
    console.error(`${context}:`, err)
    
    if (err.status === 401) {
      error.value = 'Authentication required'
      // Redirect to login
    } else if (err.status === 403) {
      error.value = 'Access denied'
    } else if (err.status === 404) {
      error.value = 'Resource not found'
    } else if (err.status >= 500) {
      error.value = 'Server error occurred'
    } else {
      error.value = err.message || 'An error occurred'
    }
  }

  function clearError() {
    error.value = null
  }

  return {
    error: readonly(error),
    isError,
    handleApiError,
    clearError
  }
}

Loading States

Loading state management and user feedback

Loading Management:

// Consistent loading pattern
export function useAsyncState<T>(
  asyncFn: () => Promise<T>,
  initialValue: T
) {
  const state = ref<T>(initialValue)
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function execute() {
    loading.value = true
    error.value = null
    
    try {
      state.value = await asyncFn()
    } catch (err: any) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }

  return {
    state: readonly(state),
    loading: readonly(loading),
    error: readonly(error),
    execute
  }
}

Caching Strategies

Studio implements intelligent caching for optimal performance.

Response Caching

API response caching strategies and invalidation

Caching Patterns:

export function useApiCache<T>() {
  const cache = new Map<string, {
    data: T
    timestamp: number
    ttl: number
  }>()

  function get(key: string): T | null {
    const entry = cache.get(key)
    if (!entry) return null
    
    if (Date.now() - entry.timestamp > entry.ttl) {
      cache.delete(key)
      return null
    }
    
    return entry.data
  }

  function set(key: string, data: T, ttl = 5 * 60 * 1000) {
    cache.set(key, {
      data,
      timestamp: Date.now(),
      ttl
    })
  }

  function invalidate(pattern?: string) {
    if (pattern) {
      for (const key of cache.keys()) {
        if (key.includes(pattern)) {
          cache.delete(key)
        }
      }
    } else {
      cache.clear()
    }
  }

  return { get, set, invalidate }
}

Optimistic Updates

Optimistic update patterns for better UX

Implementation:

async function updateMetricOptimistic(
  metricId: string, 
  updates: Partial<SemanticMetric>
) {
  // Optimistic update
  const originalMetric = currentMetric.value
  if (originalMetric) {
    currentMetric.value = { ...originalMetric, ...updates }
  }

  try {
    // API call
    const updatedMetric = await $fetch<SemanticMetric>(
      apiUrl(`/api/v1/metrics/${metricId}`),
      { method: 'PUT', body: updates }
    )
    
    // Confirm update with server response
    currentMetric.value = updatedMetric
    return updatedMetric
  } catch (err) {
    // Revert on error
    currentMetric.value = originalMetric
    throw err
  }
}

Authentication Integration

Studio handles authentication and workspace context seamlessly.

Auth State Management

Authentication state management and token handling

Auth Patterns:

export function useAuth() {
  const token = ref<string | null>(null)
  const user = ref<User | null>(null)
  const isAuthenticated = computed(() => !!token.value)

  async function login(credentials: LoginCredentials) {
    const response = await $fetch<AuthResponse>(
      apiUrl('/api/v1/auth/login'),
      { method: 'POST', body: credentials }
    )
    
    token.value = response.token
    user.value = response.user
    
    // Store in localStorage
    localStorage.setItem('auth_token', response.token)
  }

  function logout() {
    token.value = null
    user.value = null
    localStorage.removeItem('auth_token')
  }

  return {
    token: readonly(token),
    user: readonly(user),
    isAuthenticated,
    login,
    logout
  }
}

Request Interceptors

Automatic token injection and request enhancement

Implementation:

// Global fetch configuration
$fetch.create({
  onRequest({ request, options }) {
    const { token } = useAuth()
    
    if (token.value) {
      options.headers = {
        ...options.headers,
        Authorization: `Bearer ${token.value}`
      }
    }
  },
  
  onResponseError({ response }) {
    if (response.status === 401) {
      const { logout } = useAuth()
      logout()
      navigateTo('/login')
    }
  }
})

Real-time Integration

Studio supports real-time updates for collaborative features.

WebSocket Integration

Real-time updates and collaborative editing

Real-time Patterns:

export function useRealtimeMetrics() {
  const { metrics } = useMetrics()
  const ws = ref<WebSocket | null>(null)

  function connect() {
    ws.value = new WebSocket(wsUrl('/api/v1/metrics/live'))
    
    ws.value.onmessage = (event) => {
      const update = JSON.parse(event.data)
      
      switch (update.type) {
        case 'metric_updated':
          updateMetricInList(update.metric)
          break
        case 'metric_deleted':
          removeMetricFromList(update.metricId)
          break
      }
    }
  }

  function updateMetricInList(updatedMetric: SemanticMetric) {
    const index = metrics.value.findIndex(m => m.id === updatedMetric.id)
    if (index !== -1) {
      metrics.value[index] = updatedMetric
    }
  }

  return { connect, disconnect }
}

Testing Patterns

Studio demonstrates testing patterns for API integration.

Composable Testing

Testing strategies for API composables

Testing Approach:

// Mock API responses
const mockMetrics = [
  { id: '1', name: 'Test Metric', data_model_id: 'model-1' }
]

// Test composable
describe('useMetrics', () => {
  it('should fetch metrics successfully', async () => {
    // Mock fetch
    vi.mocked($fetch).mockResolvedValue({ metrics: mockMetrics })
    
    const { metrics, fetchMetrics, loading } = useMetrics()
    
    expect(loading.value).toBe(false)
    
    await fetchMetrics('model-1')
    
    expect(metrics.value).toEqual(mockMetrics)
    expect(loading.value).toBe(false)
  })
})

Best Practices

API Design

  1. Consistent Interfaces: Use consistent patterns across all composables
  2. Error Handling: Implement comprehensive error states and user feedback
  3. Loading States: Provide visual feedback during async operations
  4. Type Safety: Maintain strict TypeScript typing throughout
  5. Caching Strategy: Implement appropriate caching for performance

Performance

  1. Request Debouncing: Debounce user input to prevent excessive API calls
  2. Pagination: Implement pagination for large datasets
  3. Selective Updates: Update only changed data rather than full re-fetch
  4. Concurrent Requests: Handle multiple simultaneous requests gracefully
  5. Memory Management: Clean up subscriptions and watchers

Security

  1. Token Management: Secure token storage and automatic refresh
  2. Request Validation: Validate all outgoing requests
  3. Response Validation: Validate all incoming responses
  4. Error Sanitization: Don't expose sensitive information in errors
  5. HTTPS Only: Ensure all API communication uses HTTPS

Next Steps

Studio's API integration patterns provide a solid foundation for building maintainable, type-safe analytics interfaces that scale with your application needs.