Nitrofy LogoNitrofy
Como funcionaBeneficiosIntegracoesPlanosDuvidas
Comece agora
Nitrofy Logo
Comecar
Introduction
QuickstartEnvironment SetupRunning LocallyFirst Deploy
Organizations & TenancyAuthentication & SessionsRoles & PermissionsControllers & ProceduresJobs & QueuesPlugin ManagerContent LayerBuilt-in MCP ServerBillingNotificationsFile StorageEmailWebhooksAPI KeysSEOData FetchingDesign System
Development

Data Fetching

Comprehensive data fetching strategies with Igniter client, server actions, ISR, and real-time updates.

By the end of this guide, you'll have mastered data fetching in your SaaS application using Igniter client, server actions, ISR, and real-time updates for optimal performance and user experience.

Overview

The SaaS Boilerplate provides a comprehensive data fetching system built on Next.js App Router with Igniter.js, supporting multiple strategies for different use cases. Key features include:

  • Type-safe API client: Generated Igniter client with full TypeScript support
  • Server-side rendering: Direct database access in server components
  • Client-side hydration: React hooks with caching and real-time updates
  • Server actions: Secure mutations with progressive enhancement
  • Incremental static regeneration: Hybrid static/dynamic rendering
  • Real-time subscriptions: Live data updates via WebSockets
  • Intelligent caching: Request deduplication and smart invalidation
  • Error boundaries: Graceful error handling and retry mechanisms

The system automatically chooses the optimal fetching strategy based on context (server vs client) while maintaining type safety and performance.

Architecture

Igniter Client System

The Igniter client provides type-safe API access with automatic context switching:

// src/igniter.client.ts - Generated client
export const api = createIgniterClient<AppRouterType>({
  baseURL: process.env.NEXT_PUBLIC_IGNITER_APP_URL,
  basePATH: process.env.NEXT_PUBLIC_IGNITER_APP_BASE_PATH,
  router: () => {
    if (typeof window === 'undefined') {
      // Server-side: Direct router access (zero HTTP overhead)
      return require('./igniter.router').AppRouter
    }
    // Client-side: HTTP-based client with hooks
    return require('./igniter.schema').AppRouterSchema
  }
})

Data Fetching Strategies

The system supports multiple fetching strategies optimized for different scenarios:

Server Components: Direct database access with zero HTTP overhead Client Components: React hooks with caching and real-time updates Server Actions: Secure mutations with progressive enhancement ISR: Hybrid static/dynamic rendering for marketing content Real-time: WebSocket subscriptions for live data

Caching and Revalidation

Igniter provides intelligent caching with automatic revalidation:

// Automatic revalidation in mutations
export const createPost = igniter.mutation({
  handler: async ({ context, request }) => {
    const post = await context.database.post.create({ data: request.body })
    
    // Automatically revalidate related queries
    return response.revalidate(['posts.list']).created(post)
  }
})

Setting Up Data Fetching

Configure Environment Variables

Set up the required environment variables for the Igniter client:

# Client-side API configuration
NEXT_PUBLIC_IGNITER_APP_URL=http://localhost:3000
NEXT_PUBLIC_IGNITER_APP_BASE_PATH=/api/v1

For production deployments:

NEXT_PUBLIC_IGNITER_APP_URL=https://yourapp.com
NEXT_PUBLIC_IGNITER_APP_BASE_PATH=/api/v1

Generate Type-Safe Client

The Igniter client is automatically generated from your router. Ensure your controllers are properly defined:

// Controllers are automatically discovered and typed
export const userController = igniter.controller({
  name: 'Users',
  path: '/users',
  actions: {
    list: igniter.query({ /* ... */ }),
    getById: igniter.query({ /* ... */ }),
    create: igniter.mutation({ /* ... */ }),
    update: igniter.mutation({ /* ... */ }),
  }
})

Set Up Real-time Subscriptions (Optional)

For real-time features, configure WebSocket connections:

// Real-time subscriptions are built into Igniter hooks
api.posts.list.useRealtime({
  onMessage: (data) => {
    // Handle real-time updates
    console.log('New post:', data)
  }
})

Configure ISR for Marketing Pages

For documentation and marketing pages, enable ISR:

// src/app/docs/page.tsx
export const revalidate = 3600 // Revalidate every hour

export default async function DocsPage() {
  const docs = await api.docs.list.query()
  return <DocsContent docs={docs.data} />
}

Backend Usage (Procedures & Controllers)

Server-Side Data Fetching

In server components and API routes, use direct Igniter client calls:

// Server component with direct data access
export default async function DashboardPage() {
  // Zero HTTP overhead - direct database access
  const stats = await api.analytics.stats.query()
  const recentPosts = await api.posts.recent.query()
  
  return (
    <Dashboard 
      stats={stats.data} 
      posts={recentPosts.data} 
    />
  )
}

Controller-Based Data Operations

Define controllers with proper caching and revalidation:

// User management controller
export const userController = igniter.controller({
  name: 'Users',
  path: '/users',
  actions: {
    // Query with caching
    list: igniter.query({
      name: 'listUsers',
      description: 'Get paginated user list',
      method: 'GET',
      path: '/',
      use: [AuthFeatureProcedure()],
      handler: async ({ context, request, response }) => {
        const session = await context.auth.getSession({
          requirements: 'authenticated',
          roles: ['admin']
        })
        
        const users = await context.database.user.findMany({
          where: { organizationId: session.organization.id },
          orderBy: { createdAt: 'desc' }
        })
        
        return response.success(users)
      }
    }),
    
    // Mutation with revalidation
    create: igniter.mutation({
      name: 'createUser',
      description: 'Create new user',
      method: 'POST',
      path: '/',
      use: [AuthFeatureProcedure()],
      body: z.object({
        name: z.string(),
        email: z.string().email(),
        role: z.enum(['member', 'admin'])
      }),
      handler: async ({ context, request, response }) => {
        const session = await context.auth.getSession({
          requirements: 'authenticated',
          roles: ['admin', 'owner']
        })
        
        const user = await context.database.user.create({
          data: {
            ...request.body,
            organizationId: session.organization.id
          }
        })
        
        // Revalidate user list for all connected clients
        return response.revalidate(['users.list']).created(user)
      }
    })
  }
})

Server Actions for Mutations

Use server actions for secure form submissions:

// app/actions/user-actions.ts
'use server'

import { api } from '@/igniter.client'
import { revalidatePath } from 'next/cache'

export async function createUserAction(formData: FormData) {
  try {
    const userData = {
      name: formData.get('name') as string,
      email: formData.get('email') as string,
      role: formData.get('role') as string,
    }
    
    const result = await api.users.create.mutate({ body: userData })
    
    // Revalidate the users page
    revalidatePath('/users')
    
    return { success: true, user: result.data }
  } catch (error) {
    return { success: false, error: 'Failed to create user' }
  }
}

Frontend Usage (Client-side)

React Hooks with Caching

Use Igniter hooks in client components for automatic caching and updates:

// Client component with hooks
'use client'

import { api } from '@/igniter.client'

function UserList() {
  // Automatic caching, loading states, and error handling
  const { 
    data: users, 
    isLoading, 
    error, 
    refetch 
  } = api.users.list.useQuery({
    // Optional configuration
    staleTime: 5 * 60 * 1000, // 5 minutes
    refetchOnWindowFocus: true,
    refetchInterval: 30000, // 30 seconds for real-time feel
  })
  
  if (isLoading) return <UserListSkeleton />
  if (error) return <ErrorMessage error={error} />
  
  return (
    <div>
      <button onClick={() => refetch()}>Refresh</button>
      {users?.data.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  )
}

Mutations with Optimistic Updates

Handle mutations with automatic cache updates:

function CreateUserForm() {
  const { invalidate } = useQueryClient()
  
  const createUser = api.users.create.useMutation({
    onSuccess: (newUser) => {
      // Automatically invalidate and refetch related queries
      invalidate(['users.list'])
      toast.success('User created successfully!')
    },
    onError: (error) => {
      toast.error('Failed to create user')
    }
  })
  
  const handleSubmit = async (formData: UserFormData) => {
    await createUser.mutateAsync({ body: formData })
  }
  
  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button type="submit" disabled={createUser.isPending}>
        {createUser.isPending ? 'Creating...' : 'Create User'}
      </button>
    </form>
  )
}

Real-time Subscriptions

Subscribe to live data updates:

function NotificationBell() {
  const [notifications, setNotifications] = useState([])
  
  // Subscribe to real-time notifications
  api.notifications.list.useRealtime({
    onMessage: (update) => {
      setNotifications(prev => [update, ...prev])
    },
    onError: (error) => {
      console.error('Real-time connection failed:', error)
    }
  })
  
  return (
    <div>
      <BellIcon />
      {notifications.length > 0 && (
        <Badge count={notifications.length} />
      )}
    </div>
  )
}

Query Invalidation and Refetching

Manually control cache invalidation:

function UserProfile({ userId }: { userId: string }) {
  const { invalidate } = useQueryClient()
  
  const { data: user } = api.users.getById.useQuery({
    params: { id: userId }
  })
  
  const handleUpdateProfile = async () => {
    // Update profile logic...
    
    // Invalidate specific queries
    invalidate(['users.getById', userId])
    invalidate(['users.list']) // Also refresh user list
  }
  
  return <UserProfileForm user={user} onUpdate={handleUpdateProfile} />
}

Data Fetching Strategies

Server-Side Rendering (SSR)

For dynamic, user-specific content:

// Always server-rendered for fresh data
export default async function DashboardPage() {
  const session = await auth.getSession()
  const dashboardData = await api.dashboard.stats.query({
    params: { userId: session.user.id }
  })
  
  return <Dashboard data={dashboardData.data} />
}

Static Site Generation (SSG) with ISR

For marketing pages and documentation:

// src/app/docs/page.tsx
export const revalidate = 3600 // Revalidate every hour

export default async function DocsPage() {
  // Static generation with periodic revalidation
  const docs = await api.docs.list.query()
  
  return <DocsLayout docs={docs.data} />
}

Client-Side Fetching

For interactive features and user actions:

// Client-side fetching for interactivity
function SearchUsers() {
  const [searchTerm, setSearchTerm] = useState('')
  
  const { data: users } = api.users.search.useQuery({
    params: { q: searchTerm },
    enabled: searchTerm.length > 2, // Only search when meaningful
  })
  
  return (
    <div>
      <input 
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search users..."
      />
      {users?.data.map(user => (
        <UserResult key={user.id} user={user} />
      ))}
    </div>
  )
}

Practical Examples

Backend: E-commerce Product Management

Complete product CRUD with proper caching:

// Product controller with caching and revalidation
export const productController = igniter.controller({
  name: 'Products',
  path: '/products',
  actions: {
    list: igniter.query({
      name: 'listProducts',
      description: 'Get paginated product list',
      method: 'GET',
      path: '/',
      handler: async ({ context, request, response }) => {
        const { page = 1, limit = 20, category } = request.query
        
        const products = await context.database.product.findMany({
          where: category ? { categoryId: category } : {},
          skip: (page - 1) * limit,
          take: limit,
          include: { category: true }
        })
        
        return response.success({
          products,
          pagination: { page, limit, total: products.length }
        })
      }
    }),
    
    create: igniter.mutation({
      name: 'createProduct',
      description: 'Create new product',
      method: 'POST',
      path: '/',
      use: [AuthFeatureProcedure()],
      body: z.object({
        name: z.string(),
        price: z.number(),
        categoryId: z.string()
      }),
      handler: async ({ context, request, response }) => {
        const session = await context.auth.getSession({
          requirements: 'authenticated',
          roles: ['admin']
        })
        
        const product = await context.database.product.create({
          data: {
            ...request.body,
            organizationId: session.organization.id
          }
        })
        
        // Revalidate product lists
        return response.revalidate(['products.list']).created(product)
      }
    })
  }
})

Frontend: Real-time Chat Application

Live chat with real-time message updates:

function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState([])
  const { invalidate } = useQueryClient()
  
  // Load initial messages
  const { data: initialMessages } = api.chat.messages.useQuery({
    params: { roomId },
    staleTime: 30 * 1000 // 30 seconds
  })
  
  // Subscribe to real-time updates
  api.chat.messages.useRealtime({
    params: { roomId },
    onMessage: (newMessage) => {
      setMessages(prev => [...prev, newMessage])
    }
  })
  
  // Send message mutation
  const sendMessage = api.chat.sendMessage.useMutation({
    onSuccess: () => {
      // Message sent successfully, real-time will handle UI update
    }
  })
  
  const handleSendMessage = async (content: string) => {
    await sendMessage.mutateAsync({
      body: { roomId, content }
    })
  }
  
  const allMessages = [...(initialMessages?.data || []), ...messages]
  
  return (
    <div className="chat-room">
      <MessageList messages={allMessages} />
      <MessageInput onSend={handleSendMessage} />
    </div>
  )
}

Backend: Analytics Dashboard

Server-side analytics with caching:

// Analytics controller with smart caching
export const analyticsController = igniter.controller({
  name: 'Analytics',
  path: '/analytics',
  actions: {
    dashboard: igniter.query({
      name: 'getDashboard',
      description: 'Get dashboard analytics',
      method: 'GET',
      path: '/dashboard',
      use: [AuthFeatureProcedure()],
      handler: async ({ context, request, response }) => {
        const session = await context.auth.getSession({
          requirements: 'authenticated'
        })
        
        const { period = '30d' } = request.query
        
        // Aggregate data from multiple sources
        const [userStats, revenueStats, productStats] = await Promise.all([
          context.database.user.groupBy({
            by: ['createdAt'],
            where: { 
              organizationId: session.organization.id,
              createdAt: { gte: getDateRange(period) }
            },
            _count: true
          }),
          context.database.order.aggregate({
            where: { 
              organizationId: session.organization.id,
              createdAt: { gte: getDateRange(period) }
            },
            _sum: { total: true }
          }),
          context.database.product.findMany({
            where: { organizationId: session.organization.id },
            orderBy: { salesCount: 'desc' },
            take: 5
          })
        ])
        
        return response.success({
          users: userStats,
          revenue: revenueStats._sum.total || 0,
          topProducts: productStats
        })
      }
    })
  }
})

Data Fetching Patterns

Query Keys and Invalidation

Prop

Type

Caching Strategies

Prop

Type

Troubleshooting

Best Practices

See Also

  • Controllers and Procedures - Backend API structure
  • Authentication & Sessions - User context in data fetching
  • Jobs & Queues - Background processing for data operations
  • Notifications - Real-time updates integration
  • Content Layer - ISR for documentation and content

API Reference

Igniter Client Methods

Prop

Type

Query Client Methods

Prop

Type

Configuration Options

Prop

Type

SEO

Comprehensive SEO setup with metadata, dynamic OG images, sitemaps, structured data, and performance optimization.

Design System

Comprehensive design system with Tailwind CSS, shadcn UI components, theming, and accessibility patterns.

On this page

OverviewArchitectureIgniter Client SystemData Fetching StrategiesCaching and RevalidationSetting Up Data FetchingConfigure Environment VariablesGenerate Type-Safe ClientSet Up Real-time Subscriptions (Optional)Configure ISR for Marketing PagesBackend Usage (Procedures & Controllers)Server-Side Data FetchingController-Based Data OperationsServer Actions for MutationsFrontend Usage (Client-side)React Hooks with CachingMutations with Optimistic UpdatesReal-time SubscriptionsQuery Invalidation and RefetchingData Fetching StrategiesServer-Side Rendering (SSR)Static Site Generation (SSG) with ISRClient-Side FetchingPractical ExamplesBackend: E-commerce Product ManagementFrontend: Real-time Chat ApplicationBackend: Analytics DashboardData Fetching PatternsQuery Keys and InvalidationCaching StrategiesTroubleshootingBest PracticesSee AlsoAPI ReferenceIgniter Client MethodsQuery Client MethodsConfiguration Options
Nitrofy LogoNitrofy

Automatize o envio e a cobrança dos seus contratos

© 2026 Nitrofy, All rights reserved