with one click
data-fetching
// Server-Side + Client-Side Data Fetching with Orval + TanStack Query HydrationBoundary Pattern. ALWAYS use Orval - NEVER manual fetch()!
// Server-Side + Client-Side Data Fetching with Orval + TanStack Query HydrationBoundary Pattern. ALWAYS use Orval - NEVER manual fetch()!
Generate TypeScript API client from Swagger/Go comments. Use when updating API endpoints, adding new routes, or regenerating the frontend API client after backend changes.
Manage database migrations and Better Auth schema. Use when adding tables, modifying schema, running migrations, or resetting the database.
Generate full-stack features. Backend = hand-written bounded-context aggregates (DDD); frontend = FSD slices with HydrationBoundary. Use when creating new features, adding CRUD operations, or scaffolding new pages.
Access local technical documentation before searching the internet. Use FIRST when researching any tech stack question.
Create and manage authentication pages with server-side session handling. Use when adding login, register, or protected pages WITHOUT flicker/skeleton.
Feature-Sliced Design architecture for frontend. Use when creating new features, slices, or understanding the FSD layer structure.
| name | data-fetching |
| description | Server-Side + Client-Side Data Fetching with Orval + TanStack Query HydrationBoundary Pattern. ALWAYS use Orval - NEVER manual fetch()! |
| allowed-tools | Read, Edit, Write, Glob, Grep |
Core Rule: ALWAYS use Orval-generated functions - NEVER manual fetch() calls!
src/shared/
āāā api/ # Orval-generated
ā āāā endpoints/ # React Query Hooks
ā āāā models/ # TypeScript Types
ā āāā custom-fetch.ts # Fetch Wrapper
āāā lib/
āāā query-client.ts # getQueryClient()
āāā auth-server/ # Server-only: getSession()
āāā auth-client/ # Client-safe: signIn, signOut
Server Component (prefetchQuery) ā HydrationBoundary ā Client Component (useQuery)
shared/lib/query-client.ts)import {
isServer,
QueryClient,
defaultShouldDehydrateQuery,
} from "@tanstack/react-query"
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
dehydrate: {
// Include pending queries for streaming
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
export function getQueryClient() {
if (isServer) {
return makeQueryClient()
}
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
shared/config/providers.tsx)"use client"
import { QueryClientProvider } from "@tanstack/react-query"
import { getQueryClient } from "@shared/lib/query-client"
export function Providers({ children }: { children: ReactNode }) {
const queryClient = getQueryClient()
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
// app/(protected)/dashboard/page.tsx - SERVER COMPONENT
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { getStats, getGetStatsQueryKey } from "@shared/api/endpoints/users/users"
import { getQueryClient } from "@shared/lib/query-client"
import { getSession } from "@shared/lib/auth-server"
import { StatsGrid } from "@features/stats"
export default async function DashboardPage() {
// 1. Check session
const session = await getSession()
if (!session) redirect("/login")
// 2. Get cookies for server fetch
const cookieStore = await cookies()
const cookieHeader = cookieStore
.getAll()
.map((c) => `${c.name}=${c.value}`)
.join("; ")
// 3. Prefetch with Orval function
const queryClient = getQueryClient()
await queryClient.prefetchQuery({
queryKey: getGetStatsQueryKey(),
queryFn: () =>
getStats({
headers: { Cookie: cookieHeader },
cache: "no-store",
}),
})
// 4. Wrap with HydrationBoundary
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<StatsGrid />
</HydrationBoundary>
)
}
// features/stats/ui/stats-grid.tsx - CLIENT COMPONENT
"use client"
import { useGetStats, usePostStats } from "@shared/api/endpoints/users/users"
import { useSSE } from "../model/use-sse"
export function StatsGrid() {
// SSE for real-time updates
useSSE()
// Data is already hydrated - no initialData needed!
const { data: response } = useGetStats()
// Mutation Hook
const { mutate: updateStats } = usePostStats()
const stats = response?.status === 200 ? response.data : null
return (
<div>
<p>Projects: {stats?.projectCount}</p>
<button onClick={() => updateStats({ data: { field: "projects", delta: 1 } })}>
+1
</button>
</div>
)
}
// Server Component
export default async function DashboardPage() {
const session = await getSession()
if (!session) redirect("/login")
const cookieStore = await cookies()
const cookieHeader = cookieStore
.getAll()
.map((c) => `${c.name}=${c.value}`)
.join("; ")
const queryClient = getQueryClient()
// Parallel prefetch
await Promise.all([
queryClient.prefetchQuery({
queryKey: getGetStatsQueryKey(),
queryFn: () => getStats({ headers: { Cookie: cookieHeader }, cache: "no-store" }),
}),
queryClient.prefetchQuery({
queryKey: getGetNotificationsQueryKey(),
queryFn: () => getNotifications({ headers: { Cookie: cookieHeader }, cache: "no-store" }),
}),
])
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<StatsGrid />
<NotificationList />
</HydrationBoundary>
)
}
// ā NEVER DO THIS:
async function getStats() {
const res = await fetch("http://localhost:8080/api/v1/stats")
return res.json()
}
// ā
ALWAYS DO THIS (Orval function):
import { getStats, getGetStatsQueryKey } from "@shared/api/endpoints/users/users"
await queryClient.prefetchQuery({
queryKey: getGetStatsQueryKey(),
queryFn: () => getStats({ headers: { Cookie: cookieHeader } }),
})
ā Server-Side (prefetchQuery in Server Component):
ā Client-Side Only (useQuery without prefetch):
// features/stats/model/use-sse.ts
"use client"
import { useQueryClient } from "@tanstack/react-query"
import { useEffect } from "react"
import { getGetStatsQueryKey } from "@shared/api/endpoints/users/users"
export function useSSE() {
const queryClient = useQueryClient()
useEffect(() => {
const eventSource = new EventSource(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/events`
)
eventSource.addEventListener("stats-updated", () => {
queryClient.invalidateQueries({ queryKey: getGetStatsQueryKey() })
})
return () => eventSource.close()
}, [queryClient])
}
For streaming without await:
export default function PostsPage() {
const queryClient = getQueryClient()
// No await - starts fetch, doesn't block
queryClient.prefetchQuery({
queryKey: getGetPostsQueryKey(),
queryFn: () => getPosts(),
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Posts /> {/* useSuspenseQuery here for streaming */}
</HydrationBoundary>
)
}
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SERVER COMPONENT ā
ā 1. Check session (getSession) ā
ā 2. Get cookies for auth ā
ā 3. prefetchQuery with Orval function ā
ā 4. Wrap with HydrationBoundary ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
dehydrate(queryClient)
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā CLIENT COMPONENT ā
ā 1. useQuery() - Data is already there! ā
ā 2. useSSE() for real-time updates ā
ā 3. useMutation() for changes ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā