Best Practices
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

