# Next.js Data Fetching Standard

> **Scope:** frontend/nextjs/data-fetching
> **Layer:** 2 (on keyword)
> **Keywords:** data fetching, tanstack query, react query, server component, fetch, api, useQuery, useMutation
> **Load When:** fetching data from .NET API

**Verified against:** Next.js 15 + TanStack Query 5 + Zod 4. Last-verified: 2026-05-20.

---

Server Components for initial page load. TanStack Query for client mutations and interactive data. Never `useEffect` for fetching.

## Core Rules

- ALWAYS use Server Components for initial data that doesn't require interactivity
- ALWAYS use TanStack Query (`useQuery`, `useMutation`) for client-side data needs
- NEVER use `useEffect(() => { fetch(...) }, [])` — this is the old pattern
- ALWAYS validate API responses with Zod before using them
- ALWAYS use query key factories for consistent cache invalidation

## TanStack Query Setup

```tsx
// lib/query-client.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

export function QueryProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: { queries: { staleTime: 60 * 1000, retry: 1 } },
  }));
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
```

## Query Key Factory Pattern

```ts
// features/users/hooks/query-keys.ts
export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters: Record<string, unknown>) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
};
```

## useQuery Hook Pattern

Client Components never call the .NET backend directly — they call a same-origin proxy route. Use a
dedicated `PROXY_API_PREFIX` constant for this, never the backend's own prefix (see
[Common Mistakes](#common-mistakes) and `backend/integrations/neon-auth/neon-auth.md` →
"API Prefix Convention" for why sharing one constant between server-direct and proxy calls silently
breaks one of the two at runtime).

```ts
// features/users/hooks/use-users.ts
import { useQuery } from '@tanstack/react-query';
import { userKeys } from './query-keys';
import { z } from 'zod';
import { userSchema } from '@/features/users/types/user.schemas';
import { PROXY_API_PREFIX } from '@/lib/api/prefixes'; // client-proxy prefix ONLY — never BACKEND_API_PREFIX here

const usersResponseSchema = z.array(userSchema);

export function useUsers() {
  return useQuery({
    queryKey: userKeys.lists(),
    queryFn: async () => {
      const res = await fetch(`${PROXY_API_PREFIX}/users`);
      if (!res.ok) throw new Error('Failed to fetch users');
      return usersResponseSchema.parse(await res.json());
    },
  });
}
```

## useMutation Hook Pattern

```ts
// features/users/hooks/use-create-user.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { userKeys } from './query-keys';
import type { CreateUserInput } from '@/features/users/types/user.types';
import { PROXY_API_PREFIX } from '@/lib/api/prefixes';

export function useCreateUser() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: async (input: CreateUserInput) => {
      const res = await fetch(`${PROXY_API_PREFIX}/users`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(input),
      });
      if (!res.ok) throw new Error('Failed to create user');
      return res.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}
```

## Server Component + TanStack Query Handoff

Server Components call the .NET backend directly — use `BACKEND_API_PREFIX`, never the client-proxy
constant, and never a single shared `API_PREFIX` reused for both call sites.

```tsx
// app/(dashboard)/users/page.tsx — Server Component: initial fetch
import { BACKEND_API_PREFIX } from '@/lib/api/prefixes'; // server-direct prefix ONLY — never PROXY_API_PREFIX here

export default async function UsersPage() {
  const initialUsers = await fetch(`${BACKEND_API_PREFIX}/users`).then(r => r.json());
  return <UserListClient initialData={initialUsers} />;
}

// features/users/components/user-list-client.tsx — Client Component
'use client';
export function UserListClient({ initialData }: { initialData: User[] }) {
  const { data: users = initialData } = useUsers();
  // mutations, filtering, etc.
}
```

## Common Mistakes

| Wrong | Right | Why |
|-------|-------|-----|
| `useEffect(() => { fetch(...) }, [])` | `useQuery(...)` | Two renders, no cache, no SSR |
| No Zod on API response | `schema.parse(await res.json())` | Type safety at runtime |
| Hardcoded query keys: `['users']` | Key factory: `userKeys.lists()` | Consistent invalidation |
| `queryClient.invalidateQueries(['users'])` | `{ queryKey: userKeys.lists() }` | Object syntax required in v5 |
| One shared `API_PREFIX` for server-direct AND client-proxy fetches | Two named constants: `BACKEND_API_PREFIX` / `PROXY_API_PREFIX` | Silent runtime break on whichever side drifts — no build-time signal |

---

*MORPH-SPEC by Polymorphism Tech*
