Best Practices

API Best Practices and Performance

Guidelines for building efficient, maintainable applications with the Cloudillo API.

Authentication Best Practices

Token Management

Use appropriate token types:

// ✅ Use login tokens for initial authentication
const loginResponse = await fetch('/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password })
})
const { data } = await loginResponse.json()
const loginToken = data.token

// ✅ Use access tokens for regular API calls
const accessResponse = await fetch('/api/auth/access-token?scope=write', {
  headers: { 'Authorization': `Bearer ${loginToken}` }
})
const { data: accessData } = await accessResponse.json()
const accessToken = accessData.token

// ✅ Use proxy tokens for federation
const proxyResponse = await fetch('/api/auth/proxy-token?target=alice@other.com', {
  headers: { 'Authorization': `Bearer ${loginToken}` }
})

Implement token refresh:

class TokenManager {
  private token: string | null = null
  private refreshTimer: number | null = null

  async getToken(): Promise<string> {
    if (!this.token || this.isExpiringSoon(this.token)) {
      await this.refresh()
    }
    return this.token!
  }

  private isExpiringSoon(token: string): boolean {
    const payload = JSON.parse(atob(token.split('.')[1]))
    const expiresAt = payload.exp * 1000
    const now = Date.now()
    const fiveMinutes = 5 * 60 * 1000

    return expiresAt - now < fiveMinutes
  }

  private async refresh() {
    const response = await fetch('/api/auth/login-token', {
      headers: { 'Authorization': `Bearer ${this.token}` }
    })

    if (!response.ok) {
      // Redirect to login
      window.location.href = '/login'
      throw new Error('Session expired')
    }

    const { data } = await response.json()
    this.token = data.token

    // Schedule next refresh
    this.scheduleRefresh()
  }

  private scheduleRefresh() {
    if (this.refreshTimer) clearTimeout(this.refreshTimer)

    const payload = JSON.parse(atob(this.token!.split('.')[1]))
    const expiresAt = payload.exp * 1000
    const now = Date.now()
    const refreshAt = expiresAt - (10 * 60 * 1000) // 10 min before expiry

    this.refreshTimer = setTimeout(() => this.refresh(), refreshAt - now)
  }
}

Secure token storage:

// ✅ Use sessionStorage for temporary sessions
sessionStorage.setItem('token', token)

// ✅ Use localStorage for persistent sessions (with caution)
localStorage.setItem('token', token)

// ❌ Never store tokens in cookies without httpOnly + secure flags
// ❌ Never store tokens in URL parameters
// ❌ Never log tokens to console in production

Request Optimization

Minimize API Calls

Use query parameters to get exactly what you need:

// ❌ Bad - multiple requests for related data
const action = await api.action.id(actionId).get()
const issuer = await api.profile.id(action.issuerTag).get()
const comments = await api.action.get({ parentId: actionId })

// ✅ Good - single request with expansion
const action = await api.action.id(actionId).get({
  _expand: 'issuer,audience,parent'
})

Batch operations when possible:

// ❌ Bad - N requests
for (const actionId of actionIds) {
  await api.action.id(actionId).delete()
}

// ✅ Good - check if batch endpoint exists
await api.action.batch.post({
  operations: actionIds.map(id => ({
    method: 'DELETE',
    id
  }))
})

// ⚠️ Note: Batch endpoints may not be implemented for all resources

Use field selection to reduce response size:

// ❌ Bad - fetching unnecessary data
const actions = await api.action.get()

// ✅ Good - only fetch needed fields
const actions = await api.action.get({
  _fields: 'actionId,type,content,createdAt'
})

Efficient Pagination

Use cursor-based pagination for real-time data:

class ActionFeed {
  private lastId: string | null = null

  async loadMore() {
    const params: any = {
      _limit: 20,
      _sort: 'createdAt',
      _order: 'desc'
    }

    if (this.lastId) {
      // Fetch items after last seen
      params.afterId = this.lastId
    }

    const { data } = await api.action.get(params)

    if (data.length > 0) {
      this.lastId = data[data.length - 1].actionId
    }

    return data
  }
}

Implement infinite scroll efficiently:

class InfiniteScroll {
  private loading = false
  private hasMore = true

  async loadPage() {
    if (this.loading || !this.hasMore) return

    this.loading = true

    try {
      const { data, pagination } = await api.action.get({
        _limit: 20,
        _offset: this.items.length
      })

      this.items.push(...data)
      this.hasMore = pagination.hasMore
    } finally {
      this.loading = false
    }
  }

  setupScrollListener() {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          this.loadPage()
        }
      },
      { threshold: 1.0 }
    )

    observer.observe(this.sentinelElement)
  }
}

Caching Strategies

Client-Side Caching

Cache static data:

class ProfileCache {
  private cache = new Map<string, { data: any; timestamp: number }>()
  private TTL = 5 * 60 * 1000 // 5 minutes

  async get(idTag: string) {
    const cached = this.cache.get(idTag)

    if (cached && Date.now() - cached.timestamp < this.TTL) {
      return cached.data
    }

    const profile = await api.profile.id(idTag).get()
    this.cache.set(idTag, { data: profile, timestamp: Date.now() })

    return profile
  }

  invalidate(idTag: string) {
    this.cache.delete(idTag)
  }

  invalidateAll() {
    this.cache.clear()
  }
}

Use ETags for conditional requests:

class ConditionalFetch {
  private etags = new Map<string, string>()

  async fetch(url: string, options: RequestInit = {}) {
    const etag = this.etags.get(url)

    if (etag) {
      options.headers = {
        ...options.headers,
        'If-None-Match': etag
      }
    }

    const response = await fetch(url, options)

    if (response.status === 304) {
      // Not modified - use cached data
      return this.getCachedData(url)
    }

    const newEtag = response.headers.get('ETag')
    if (newEtag) {
      this.etags.set(url, newEtag)
    }

    return response
  }
}

Implement request deduplication:

class RequestDeduplicator {
  private pending = new Map<string, Promise<any>>()

  async fetch(url: string, options?: RequestInit) {
    const key = JSON.stringify({ url, options })

    if (this.pending.has(key)) {
      // Return existing promise
      return this.pending.get(key)!
    }

    const promise = fetch(url, options)
      .then(r => r.json())
      .finally(() => this.pending.delete(key))

    this.pending.set(key, promise)
    return promise
  }
}

Server-Side Caching

Use appropriate cache headers:

// For CDN caching of public resources
fetch('/api/profile/alice@example.com', {
  headers: {
    'Cache-Control': 'public, max-age=300'  // 5 min
  }
})

// For private user data
fetch('/api/me', {
  headers: {
    'Cache-Control': 'private, no-cache'
  }
})

WebSocket Best Practices

Connection Management

Implement reconnection logic:

class ResilientWebSocket {
  private ws: WebSocket | null = null
  private reconnectAttempts = 0
  private maxReconnectAttempts = 5
  private reconnectDelay = 1000

  connect(url: string, token: string) {
    this.ws = new WebSocket(`${url}?token=${token}`)

    this.ws.onopen = () => {
      console.log('WebSocket connected')
      this.reconnectAttempts = 0
      this.reconnectDelay = 1000
    }

    this.ws.onclose = (event) => {
      console.log('WebSocket closed:', event.code, event.reason)

      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++
        setTimeout(() => this.connect(url, token), this.reconnectDelay)
        this.reconnectDelay *= 2  // Exponential backoff
      }
    }

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error)
    }

    this.ws.onmessage = (event) => {
      this.handleMessage(JSON.parse(event.data))
    }
  }

  send(data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
    } else {
      console.error('WebSocket not connected')
    }
  }

  disconnect() {
    this.reconnectAttempts = this.maxReconnectAttempts  // Prevent reconnect
    this.ws?.close()
  }
}

Heartbeat to keep connection alive:

class WebSocketWithHeartbeat extends ResilientWebSocket {
  private heartbeatInterval: number | null = null
  private heartbeatTimeout: number | null = null

  connect(url: string, token: string) {
    super.connect(url, token)
    this.startHeartbeat()
  }

  private startHeartbeat() {
    this.heartbeatInterval = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.send({ type: 'ping' })

        // Expect pong within 5 seconds
        this.heartbeatTimeout = setTimeout(() => {
          console.error('Heartbeat timeout - reconnecting')
          this.ws?.close()
        }, 5000)
      }
    }, 30000)  // Every 30 seconds
  }

  handleMessage(data: any) {
    if (data.type === 'pong') {
      if (this.heartbeatTimeout) {
        clearTimeout(this.heartbeatTimeout)
      }
      return
    }

    super.handleMessage(data)
  }

  disconnect() {
    if (this.heartbeatInterval) clearInterval(this.heartbeatInterval)
    if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout)
    super.disconnect()
  }
}

Error Handling

Graceful Degradation

Handle API failures gracefully:

class RobustApiClient {
  async fetchWithFallback(url: string, options?: RequestInit) {
    try {
      const response = await fetch(url, options)

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }

      return await response.json()
    } catch (error) {
      console.error('API request failed:', error)

      // Return cached data if available
      const cached = this.getFromCache(url)
      if (cached) {
        console.log('Using cached data')
        return cached
      }

      // Return default data
      return this.getDefaultData(url)
    }
  }

  private getDefaultData(url: string) {
    // Return sensible defaults based on endpoint
    if (url.includes('/api/action')) {
      return { data: [], pagination: { total: 0, hasMore: false } }
    }
    return null
  }
}

Implement retry logic:

async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  maxRetries = 3
) {
  let lastError: Error

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options)

      // Retry on 5xx errors
      if (response.status >= 500) {
        throw new Error(`Server error: ${response.status}`)
      }

      // Don't retry on 4xx errors (client errors)
      if (!response.ok) {
        const { error } = await response.json()
        throw new Error(error.message)
      }

      return response
    } catch (error) {
      lastError = error as Error

      // Exponential backoff: 1s, 2s, 4s
      if (attempt < maxRetries - 1) {
        const delay = Math.pow(2, attempt) * 1000
        await new Promise(resolve => setTimeout(resolve, delay))
      }
    }
  }

  throw lastError!
}

User Feedback

Show appropriate loading states:

class LoadingState {
  private loading = new Set<string>()

  async execute<T>(key: string, fn: () => Promise<T>): Promise<T> {
    this.loading.add(key)
    this.updateUI()

    try {
      return await fn()
    } catch (error) {
      this.handleError(error)
      throw error
    } finally {
      this.loading.delete(key)
      this.updateUI()
    }
  }

  private updateUI() {
    const isLoading = this.loading.size > 0
    document.body.classList.toggle('loading', isLoading)

    // Update specific loading indicators
    for (const key of this.loading) {
      const element = document.querySelector(`[data-loading="${key}"]`)
      element?.classList.add('loading')
    }
  }

  private handleError(error: any) {
    // Show user-friendly error message
    const message = this.getErrorMessage(error)
    this.showToast(message, 'error')
  }

  private getErrorMessage(error: any): string {
    if (error.code === 'E-AUTH-UNAUTH') {
      return 'Please log in to continue'
    }
    if (error.code === 'E-AUTH-FORBIDDEN') {
      return 'You do not have permission to perform this action'
    }
    if (error.code === 'E-RATELIMIT') {
      return 'Too many requests. Please try again later.'
    }
    return 'An error occurred. Please try again.'
  }
}

File Upload Optimization

Chunk Large Files

class ChunkedUploader {
  async upload(file: File, preset: string = 'default') {
    const chunkSize = 5 * 1024 * 1024  // 5MB chunks
    const totalChunks = Math.ceil(file.size / chunkSize)

    for (let i = 0; i < totalChunks; i++) {
      const start = i * chunkSize
      const end = Math.min(start + chunkSize, file.size)
      const chunk = file.slice(start, end)

      await this.uploadChunk(chunk, i, totalChunks, file.name, preset)

      this.onProgress(i + 1, totalChunks)
    }

    return this.finalizeUpload(file.name)
  }

  private async uploadChunk(
    chunk: Blob,
    index: number,
    total: number,
    fileName: string,
    preset: string
  ) {
    const formData = new FormData()
    formData.append('file', chunk)

    await fetch(`/api/file/${preset}/${fileName}`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'X-Chunk-Index': index.toString(),
        'X-Chunk-Total': total.toString()
      },
      body: formData
    })
  }
}

Image Optimization

async function optimizeImage(file: File): Promise<Blob> {
  // Resize large images before upload
  const MAX_WIDTH = 2048
  const MAX_HEIGHT = 2048

  return new Promise((resolve) => {
    const img = new Image()
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')!

    img.onload = () => {
      let { width, height } = img

      if (width > MAX_WIDTH || height > MAX_HEIGHT) {
        const ratio = Math.min(MAX_WIDTH / width, MAX_HEIGHT / height)
        width *= ratio
        height *= ratio
      }

      canvas.width = width
      canvas.height = height
      ctx.drawImage(img, 0, 0, width, height)

      canvas.toBlob((blob) => resolve(blob!), file.type, 0.9)
    }

    img.src = URL.createObjectURL(file)
  })
}

// Usage
const optimized = await optimizeImage(file)
const formData = new FormData()
formData.append('file', optimized, file.name)

Performance Monitoring

Track API Performance

class PerformanceMonitor {
  track(operation: string, fn: () => Promise<any>) {
    const start = performance.now()

    return fn()
      .then(result => {
        const duration = performance.now() - start
        this.log(operation, 'success', duration)
        return result
      })
      .catch(error => {
        const duration = performance.now() - start
        this.log(operation, 'error', duration, error)
        throw error
      })
  }

  private log(operation: string, status: string, duration: number, error?: any) {
    console.log(`[PERF] ${operation}: ${status} (${duration.toFixed(2)}ms)`)

    // Send to analytics
    if (typeof window.analytics !== 'undefined') {
      window.analytics.track('API Call', {
        operation,
        status,
        duration,
        error: error?.message
      })
    }
  }
}

// Usage
const monitor = new PerformanceMonitor()

await monitor.track('fetch-actions', () =>
  api.action.get({ type: 'POST', _limit: 20 })
)

Optimize Bundle Size

// ✅ Tree-shakeable imports
import { createApiClient } from '@cloudillo/base'

// ❌ Import entire library
import * as cloudillo from '@cloudillo/base'

// ✅ Dynamic imports for large features
const ImageEditor = lazy(() => import('./ImageEditor'))

// ✅ Code splitting by route
const routes = [
  {
    path: '/profile',
    component: lazy(() => import('./pages/Profile'))
  },
  {
    path: '/feed',
    component: lazy(() => import('./pages/Feed'))
  }
]

Security Best Practices

Input Validation

function validateActionInput(action: any): string[] {
  const errors: string[] = []

  // Required fields
  if (!action.type) {
    errors.push('Action type is required')
  }

  // Valid action type
  const validTypes = ['POST', 'COMMENT', 'REACT', 'CONNECT', 'FOLLOW']
  if (action.type && !validTypes.includes(action.type)) {
    errors.push(`Invalid action type: ${action.type}`)
  }

  // Timestamp validation
  if (action.published) {
    const now = Math.floor(Date.now() / 1000)
    if (action.published > now + 300) {  // 5 min in future
      errors.push('Published timestamp cannot be in the future')
    }
  }

  // Content length limits
  if (action.content?.text?.length > 10000) {
    errors.push('Content text too long (max 10000 characters)')
  }

  return errors
}

Sanitize User Input

function sanitizeText(text: string): string {
  // Remove potential XSS vectors
  return text
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

// Use DOMPurify for HTML content
import DOMPurify from 'dompurify'

function renderUserHtml(html: string): string {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
    ALLOWED_ATTR: ['href']
  })
}

Testing Best Practices

Mock API Responses

class MockApiClient {
  private mocks = new Map<string, any>()

  mock(url: string, response: any) {
    this.mocks.set(url, response)
  }

  async fetch(url: string, options?: RequestInit) {
    const mock = this.mocks.get(url)

    if (mock) {
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mock)
      } as Response)
    }

    return fetch(url, options)
  }
}

// Usage in tests
const api = new MockApiClient()

api.mock('/api/me', {
  data: {
    idTag: 'alice@example.com',
    name: 'Alice',
    status: 'A'
  }
})

const profile = await api.fetch('/api/me')

Integration Testing

describe('Action API', () => {
  let token: string

  beforeAll(async () => {
    // Setup: Login
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: 'test@example.com',
        password: 'testpass'
      })
    })
    const { data } = await response.json()
    token = data.token
  })

  test('create post action', async () => {
    const response = await fetch('/api/action', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        type: 'POST',
        content: { text: 'Test post' },
        audience: ['public'],
        published: Math.floor(Date.now() / 1000)
      })
    })

    expect(response.ok).toBe(true)

    const { data } = await response.json()
    expect(data.actionId).toBeDefined()
    expect(data.type).toBe('POST')
  })
})

Code Organization

API Client Abstraction

// api/client.ts
export class CloudilloClient {
  constructor(private baseUrl: string, private tokenManager: TokenManager) {}

  get actions() {
    return new ActionService(this)
  }

  get profiles() {
    return new ProfileService(this)
  }

  get files() {
    return new FileService(this)
  }

  async fetch(path: string, options: RequestInit = {}) {
    const token = await this.tokenManager.getToken()

    return fetch(`${this.baseUrl}${path}`, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${token}`
      }
    })
  }
}

// api/services/action.ts
export class ActionService {
  constructor(private client: CloudilloClient) {}

  async list(params?: ActionQuery) {
    const query = new URLSearchParams(params as any)
    const response = await this.client.fetch(`/api/action?${query}`)
    return response.json()
  }

  async create(action: CreateAction) {
    const response = await this.client.fetch('/api/action', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(action)
    })
    return response.json()
  }

  id(actionId: string) {
    return {
      get: async () => {
        const response = await this.client.fetch(`/api/action/${actionId}`)
        return response.json()
      },
      delete: async () => {
        await this.client.fetch(`/api/action/${actionId}`, { method: 'DELETE' })
      }
    }
  }
}

Summary

Key takeaways for building with Cloudillo API:

  1. Authentication: Use appropriate token types, implement refresh logic, store securely
  2. Performance: Minimize requests, use caching, implement pagination efficiently
  3. Reliability: Handle errors gracefully, implement retry logic, show user feedback
  4. Security: Validate input, sanitize content, follow OWASP guidelines
  5. Testing: Mock API calls, write integration tests, test error scenarios
  6. Code Quality: Abstract API logic, use TypeScript, follow consistent patterns

Following these practices will help you build robust, performant applications on Cloudillo.