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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
// 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:
- Authentication: Use appropriate token types, implement refresh logic, store securely
- Performance: Minimize requests, use caching, implement pagination efficiently
- Reliability: Handle errors gracefully, implement retry logic, show user feedback
- Security: Validate input, sanitize content, follow OWASP guidelines
- Testing: Mock API calls, write integration tests, test error scenarios
- Code Quality: Abstract API logic, use TypeScript, follow consistent patterns
Following these practices will help you build robust, performant applications on Cloudillo.