API Integration
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
- Consistent Interfaces: Use consistent patterns across all composables
- Error Handling: Implement comprehensive error states and user feedback
- Loading States: Provide visual feedback during async operations
- Type Safety: Maintain strict TypeScript typing throughout
- Caching Strategy: Implement appropriate caching for performance
Performance
- Request Debouncing: Debounce user input to prevent excessive API calls
- Pagination: Implement pagination for large datasets
- Selective Updates: Update only changed data rather than full re-fetch
- Concurrent Requests: Handle multiple simultaneous requests gracefully
- Memory Management: Clean up subscriptions and watchers
Security
- Token Management: Secure token storage and automatic refresh
- Request Validation: Validate all outgoing requests
- Response Validation: Validate all incoming responses
- Error Sanitization: Don't expose sensitive information in errors
- HTTPS Only: Ensure all API communication uses HTTPS
Next Steps
- UI Components - Explore Studio's custom component library
- Data Flow - Understand data flow patterns
- Core Concepts - Review architectural foundations
Studio's API integration patterns provide a solid foundation for building maintainable, type-safe analytics interfaces that scale with your application needs.