Skip to main content
Learn effective patterns for integrating your Starter Kit with the Cloud API.

Server-side data fetching

Fetch data in Server Components for better performance:
app/dashboard/page.tsx
import { hydrateDeploymentMode } from '@/lib/deployment-mode'
import { cookies } from 'next/headers'

export default async function DashboardPage() {
  const config = await hydrateDeploymentMode()
  const token = cookies().get('devkit4ai-token')?.value
  
  // Fetch data on the server
  const response = await fetch(`${config.backendApiUrl}/api/v1/projects`, {
    headers: {
      'Authorization': `Bearer ${token}`,
      ...config.headers
    },
    // Add caching if data doesn't change often
    next: { revalidate: 60 } // Revalidate every 60 seconds
  })
  
  const projects = await response.json()
  
  return (
    <div>
      {projects.map(project => (
        <ProjectCard key={project.id} project={project} />
      ))}
    </div>
  )
}
Benefits:
  • Faster page loads (no client-side waterfalls)
  • Better SEO (content rendered on server)
  • Reduced client bundle size
  • Secure (API credentials never exposed to browser)

Client-side mutations

Use Server Actions for data mutations:
app/actions.ts
'use server'

import { hydrateDeploymentMode } from '@/lib/deployment-mode'
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'

export async function createProject(formData: FormData) {
  const config = await hydrateDeploymentMode()
  const token = cookies().get('devkit4ai-token')?.value
  
  const name = formData.get('name') as string
  const description = formData.get('description') as string
  
  const response = await fetch(`${config.backendApiUrl}/api/v1/projects`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      ...config.headers,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name, description })
  })
  
  if (!response.ok) {
    const error = await response.json()
    return { error: error.detail || 'Failed to create project' }
  }
  
  // Revalidate the projects page to show new data
  revalidatePath('/console/projects')
  
  const project = await response.json()
  return { success: true, project }
}
Use in a form:
components/create-project-form.tsx
'use client'

import { createProject } from '@/app/actions'
import { useState } from 'react'

export function CreateProjectForm() {
  const [error, setError] = useState('')
  
  async function handleSubmit(formData: FormData) {
    const result = await createProject(formData)
    
    if (result.error) {
      setError(result.error)
    } else {
      // Success - form will reset and page will revalidate
    }
  }
  
  return (
    <form action={handleSubmit}>
      {error && <div className="text-red-600">{error}</div>}
      <input name="name" required />
      <textarea name="description" />
      <button type="submit">Create Project</button>
    </form>
  )
}

Optimistic updates

Update UI immediately, then sync with server:
components/like-button.tsx
'use client'

import { useState, useTransition } from 'react'
import { likePost } from '@/app/actions'

export function LikeButton({ postId, initialLikes }: Props) {
  const [likes, setLikes] = useState(initialLikes)
  const [isPending, startTransition] = useTransition()
  
  function handleLike() {
    // Optimistically update UI
    setLikes(prev => prev + 1)
    
    // Then sync with server
    startTransition(async () => {
      const result = await likePost(postId)
      
      if (!result.success) {
        // Revert on error
        setLikes(prev => prev - 1)
      }
    })
  }
  
  return (
    <button onClick={handleLike} disabled={isPending}>
      ❤️ {likes}
    </button>
  )
}

Error handling patterns

Handle errors gracefully at multiple levels:
app/actions.ts
'use server'

export async function fetchUserData() {
  try {
    const response = await fetch(apiUrl, {
      headers: { /* ... */ },
      // Timeout after 10 seconds
      signal: AbortSignal.timeout(10000)
    })
    
    // Handle HTTP errors
    if (!response.ok) {
      if (response.status === 401) {
        return { error: 'Session expired. Please log in again.' }
      }
      if (response.status === 403) {
        return { error: 'You do not have permission to view this resource.' }
      }
      if (response.status === 429) {
        return { error: 'Too many requests. Please try again later.' }
      }
      if (response.status >= 500) {
        return { error: 'Server error. Please try again later.' }
      }
      
      const error = await response.json()
      return { error: error.detail || 'Request failed' }
    }
    
    return await response.json()
  } catch (error) {
    // Handle network errors
    if (error.name === 'AbortError') {
      return { error: 'Request timed out. Please try again.' }
    }
    
    console.error('API error:', error)
    return { error: 'Unable to connect to server. Please check your internet connection.' }
  }
}

Caching strategies

Choose the right caching approach: Static Generation (ISR):
// Revalidate every hour
const response = await fetch(url, {
  next: { revalidate: 3600 }
})
On-Demand Revalidation:
import { revalidatePath, revalidateTag } from 'next/cache'

// Revalidate specific path
revalidatePath('/projects')

// Revalidate by tag
revalidateTag('projects')
No caching:
// Always fetch fresh data
const response = await fetch(url, {
  cache: 'no-store'
})

Rate limit handling

Implement exponential backoff for rate limits:
async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3
) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options)
    
    if (response.status !== 429) {
      return response
    }
    
    // Exponential backoff: 1s, 2s, 4s
    const delay = Math.pow(2, i) * 1000
    await new Promise(resolve => setTimeout(resolve, delay))
  }
  
  throw new Error('Rate limit exceeded after retries')
}

Next steps