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
- Never use raw
fetch+useState+useEffectfor data fetching - All client data goes through TanStack Query — provides caching, deduplication, and background refetching
- Query keys are centralized in
lib/query-keys.tsfor consistent cache invalidation - 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]