Data Fetching

Server-side Prisma queries and client-side TanStack Query hooks.

LaunchFst uses a clear separation between server and client data fetching.

Server-Side: Prisma

All server components and API routes use Prisma directly via the db client:

import { db } from "@/lib/db"

const users = await db.user.findMany({
  select: { id: true, name: true, email: true },
  take: 10,
})

Always use select — never return all fields implicitly. This ensures type safety and minimizes data transfer.

For parallel queries, use Promise.all:

const [users, orgs, stats] = await Promise.all([
  db.user.count(),
  db.organization.count(),
  db.contactSubmission.count({ where: { read: false } }),
])

Client-Side: TanStack Query

All client data fetching uses TanStack Query v5 hooks from hooks/core/:

"use client"

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { api } from "@/lib/api"
import { queryKeys } from "@/lib/query-keys"
import { toast } from "sonner"

// Query example
export function useOrganizations() {
  return useQuery({
    queryKey: queryKeys.organizations.list(),
    queryFn: () => api.get("/api/organizations"),
  })
}

// Mutation example
export function useUpdateProfile() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: (data) => api.put("/api/user/profile", data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: queryKeys.user.profile() })
      toast.success("Profile updated")
    },
    onError: (error) => toast.error(error.message),
  })
}

Rules

  1. Never use raw fetch + useState + useEffect for data fetching
  2. All client data goes through TanStack Query — provides caching, deduplication, and background refetching
  3. Query keys are centralized in lib/query-keys.ts for consistent cache invalidation
  4. The API client (lib/api.ts) handles JSON parsing, error handling, and 401 redirects

API Client

The api object from lib/api.ts wraps fetch with consistent error handling:

import { api } from "@/lib/api"

const data = await api.get<User[]>("/api/users")
const result = await api.post<{ id: string }>("/api/users", { name: "New User" })

All API responses follow the { success: boolean, data?: T, error?: string } pattern. The client throws ApiError if !res.ok or !json.success.

Query Keys

import { queryKeys } from "@/lib/query-keys"

queryKeys.user.profile()           // ["user", "profile"]
queryKeys.organizations.list()     // ["organizations", "list"]
queryKeys.organizations.detail(id) // ["organizations", "detail", id]
queryKeys.superadmin.users(params) // ["superadmin", "users", params]

Demo Mode — Explore freely. Some actions are restricted. demo@launchfst.dev / demo1234

Get LaunchFst →