TentoCMS
Api

Best Practices

Field selection, caching, redirects, error boundaries, pagination and rate-limit backoff.

1. Use Field Selection

Problem: Fetching full page content when you only need title and excerpt wastes bandwidth.

Solution: Use fields parameter:

// Instead of this (returns all fields)
const pages = await fetch('/api/v1/pages')

// Do this (returns only needed fields)
const pages = await fetch('/api/v1/pages?fields=title,excerpt,publishDate')

Impact: Up to 90% reduction in payload size.

2. Implement Proper Caching

Problem: Fetching same data repeatedly wastes API quota and slows down your app.

Solution: Implement multi-layer caching:

// Layer 1: CDN (Cloudflare)
// Automatic, respect Cache-Control headers

// Layer 2: Server-side cache (for SSR)
const cachedPages = await redis.get('pages:all')
if (cachedPages) return JSON.parse(cachedPages)

const pages = await fetchFromAPI('/api/v1/pages')
await redis.set('pages:all', JSON.stringify(pages), 'EX', 60)

// Layer 3: Client-side cache (SWR, React Query)
const { data } = useSWR('/api/v1/pages', fetcher, {
  revalidateOnFocus: false,
  dedupingInterval: 60000
})

3. Handle Redirects for SEO

Problem: Old slugs return 200 but SEO suffers without proper redirects.

Solution: Implement framework-level redirects:

// Next.js
export async function getStaticProps({ params }) {
  const response = await fetch(`/api/v1/pages/${params.slug}`)
  const { data, redirect } = await response.json()

  if (redirect) {
    return {
      redirect: {
        destination: `/${redirect.to}`,
        permanent: true // 301 redirect
      }
    }
  }

  return { props: { page: data } }
}

4. Implement Error Boundaries

Problem: API errors crash your entire app.

Solution: Use error boundaries and fallbacks:

async function fetchPage(slug) {
  try {
    const response = await fetch(`/api/v1/pages/${slug}`, {
      headers: { 'X-API-Key': process.env.CMS_API_KEY }
    })

    if (response.status === 404) {
      return { notFound: true }
    }

    if (response.status === 401) {
      console.error('API key invalid')
      return { error: 'Configuration error' }
    }

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`)
    }

    return await response.json()
  } catch (error) {
    console.error('Failed to fetch page:', error)
    return { error: 'Failed to load content' }
  }
}

5. Use Environment Variables

Problem: API keys hardcoded in source code are security risks.

Solution: Use environment variables:

# .env.local
CMS_API_KEY=tento_pk_your_key_here
CMS_API_BASE_URL=https://tento-api.intelligentlending.co.uk/api/v1
// lib/api.js
const API_KEY = process.env.CMS_API_KEY
const BASE_URL = process.env.CMS_API_BASE_URL

export async function fetchPages() {
  return fetch(`${BASE_URL}/pages`, {
    headers: { 'X-API-Key': API_KEY }
  })
}

6. Paginate Large Result Sets

Problem: Fetching 1000 pages in one request times out.

Solution: Use pagination:

async function fetchAllPages() {
  const allPages = []
  let page = 1
  const limit = 100

  while (true) {
    const response = await fetch(
      `/api/v1/pages?page=${page}&limit=${limit}`
    )
    const { data, pagination } = await response.json()

    allPages.push(...data)

    if (pagination.page >= pagination.totalPages) break
    page += 1
  }

  return allPages
}

7. Use Conditional Requests

Problem: Fetching unchanged content wastes bandwidth.

Solution: Use If-None-Match with ETags:

let etag = null

async function fetchPageIfModified(slug) {
  const headers = { 'X-API-Key': process.env.CMS_API_KEY }

  if (etag) {
    headers['If-None-Match'] = etag
  }

  const response = await fetch(`/api/v1/pages/${slug}`, { headers })

  if (response.status === 304) {
    // Not modified, use cached version
    return cachedPage
  }

  etag = response.headers.get('ETag')
  const data = await response.json()
  cachedPage = data

  return data
}

8. Handle Rate Limits Gracefully

Problem: Hitting rate limits stops your app from working.

Solution: Implement retry with backoff:

async function fetchWithBackoff(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options)

    if (response.status !== 429) {
      return response
    }

    const retryAfter = parseInt(response.headers.get('Retry-After') || '1')
    const backoffTime = Math.min(retryAfter * 1000, 2 ** attempt * 1000)

    console.log(`Rate limited, retrying in ${backoffTime}ms`)
    await new Promise(resolve => setTimeout(resolve, backoffTime))
  }

  throw new Error('Max retries exceeded')
}

9. Monitor API Usage

Problem: You don't know when you're approaching rate limits.

Solution: Track API usage:

class APIMonitor {
  constructor() {
    this.requests = []
  }

  async fetch(url, options) {
    const start = Date.now()

    try {
      const response = await fetch(url, options)
      const duration = Date.now() - start

      this.requests.push({
        url,
        status: response.status,
        duration,
        timestamp: Date.now()
      })

      this.logStats()

      return response
    } catch (error) {
      this.requests.push({
        url,
        status: 'error',
        error: error.message,
        timestamp: Date.now()
      })
      throw error
    }
  }

  logStats() {
    const lastMinute = this.requests.filter(
      r => Date.now() - r.timestamp < 60000
    )

    console.log(`API requests in last minute: ${lastMinute.length}/100`)
  }
}

10. Use TypeScript for Type Safety

Problem: Runtime errors from incorrect API response handling.

Solution: Define TypeScript interfaces:

interface Page {
  id: string
  slug: string
  type: string
  fields: Record<string, unknown>
  publishedAt: string
  updatedAt: string
}

interface PageListResponse {
  data: Page[]
  pagination: {
    total: number
    page: number
    limit: number
    totalPages: number
  }
}

interface PageResponse {
  data: Page
  redirect: {
    from: string
    to: string
    permanent: boolean
  } | null
}

async function fetchPages(): Promise<PageListResponse> {
  const response = await fetch('/api/v1/pages', {
    headers: { 'X-API-Key': process.env.CMS_API_KEY! }
  })

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`)
  }

  return response.json()
}

Error handling

Handle 401 Unauthorized:

const response = await fetch(url, {
  headers: { 'X-API-Key': apiKey }
})

if (response.status === 401) {
  // API key invalid or expired
  // Check key in admin UI
  // Rotate to new key if expired
  console.error('API key invalid or expired')
  // Show error to user or fail gracefully
}

Handle 404 Not Found:

if (response.status === 404) {
  // Page doesn't exist or not published
  // Show 404 page to user
  return { notFound: true }
}

Handle 429 Rate Limited:

if (response.status === 429) {
  const retryAfter = response.headers.get('Retry-After')
  // Wait and retry
  await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
  return fetch(url, options) // Retry
}

Handle 500 Internal Error:

if (response.status >= 500) {
  // Server error, implement retry with exponential backoff
  for (let i = 0; i < 3; i++) {
    await new Promise(resolve => setTimeout(resolve, 2 ** i * 1000))
    const retry = await fetch(url, options)
    if (retry.ok) return retry
  }
  // All retries failed, show error to user
}

Avoiding rate limits

1. Implement Exponential Backoff

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options)

    if (response.status !== 429) {
      return response
    }

    const retryAfter = response.headers.get('Retry-After') || 2 ** i
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
  }

  throw new Error('Max retries exceeded')
}

2. Implement Request Queuing

class RateLimiter {
  constructor(maxPerMinute = 100) {
    this.maxPerMinute = maxPerMinute
    this.queue = []
    this.requests = []
  }

  async throttle() {
    const now = Date.now()
    this.requests = this.requests.filter(time => now - time < 60000)

    if (this.requests.length >= this.maxPerMinute) {
      const oldestRequest = Math.min(...this.requests)
      const waitTime = 60000 - (now - oldestRequest)
      await new Promise(resolve => setTimeout(resolve, waitTime))
    }

    this.requests.push(Date.now())
  }

  async fetch(url, options) {
    await this.throttle()
    return fetch(url, options)
  }
}

// Usage
const limiter = new RateLimiter(100)
const response = await limiter.fetch(url, options)

3. Use Field Selection

Reduce API calls by requesting only needed fields:

# Instead of multiple requests for different data
GET /api/v1/pages/page1
GET /api/v1/pages/page2
GET /api/v1/pages/page3

# Use list endpoint with field selection
GET /api/v1/pages?fields=title,excerpt&limit=100

4. Implement Client-Side Caching

class CachedAPIClient {
  constructor(apiKey, cacheTTL = 60000) {
    this.apiKey = apiKey
    this.cacheTTL = cacheTTL
    this.cache = new Map()
  }

  async fetch(url) {
    const cached = this.cache.get(url)

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

    const response = await fetch(url, {
      headers: { 'X-API-Key': this.apiKey }
    })

    const data = await response.json()
    this.cache.set(url, { data, timestamp: Date.now() })

    return data
  }
}

5. Batch Operations

Instead of fetching pages one by one, use list endpoint:

# Bad: 10 individual requests
GET /api/v1/pages/page1
GET /api/v1/pages/page2
...
GET /api/v1/pages/page10

# Good: 1 request with pagination
GET /api/v1/pages?limit=10

Copyright © 2026