> ## Documentation Index
> Fetch the complete documentation index at: https://docs.contafy.com.mx/llms.txt
> Use this file to discover all available pages before exploring further.

# Data fetching

> Learn how Contafy fetches and manages data with Server Components, TanStack Query, and the API client architecture

Contafy uses a hybrid data fetching strategy that combines Server Components for initial data loads with TanStack Query for client-side interactivity.

## Data fetching strategy

### Server Components for initial data

Server Components fetch data directly on the server before rendering:

**Benefits**:

* No client-side loading states on initial render
* Better SEO (content in HTML)
* Reduced client bundle size
* Direct access to backend API

**Example from `app/dashboard/components/DashboardContent.tsx:50-57`**:

```tsx theme={null}
export async function DashboardContent({ profileId, mes, año }) {
  // Fetch all data in parallel on the server
  const [metrics, invoices, expenses, profiles, trendData, currentUser] = await Promise.all([
    getMetrics(profileId, mes, año),
    getInvoices({ profileId, mes, año, limit: 3 }),
    getExpenses({ profileId, mes, año, limit: 3 }),
    getProfiles(),
    getTrendData(profileId, año, 'año-actual', mes),
    getCurrentUser(),
  ]);

  return (
    <>
      <MetricsCards metrics={metrics} />
      <RecentInvoicesTable invoices={invoices.data} />
      <RecentExpensesTable expenses={expenses.data} />
    </>
  );
}
```

**Key points**:

* Use `Promise.all()` to fetch data in parallel
* No loading states needed (data ready before render)
* Pass data as props to child components
* Great for SEO and initial page load

### TanStack Query for client-side data

Client Components use TanStack Query for interactive data:

**Benefits**:

* Automatic caching and revalidation
* Background refetching
* Optimistic updates
* Request deduplication
* Built-in loading/error states

**Example from `app/dashboard/expenses/components/ExpensesListContent.tsx`**:

```tsx theme={null}
'use client';

import { useQuery } from '@tanstack/react-query';

export function ExpensesListContent() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['expenses', profileId, mes, año],
    queryFn: () => getExpenses({ profileId, mes, año }),
  });

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;

  return <ExpensesTable expenses={data.data} />;
}
```

**Key points**:

* Must be in a Client Component (`'use client'`)
* Provides loading and error states
* Automatically caches results
* Revalidates on window focus (configurable)

## API client architecture

Contafy has two API clients: one for Server Components and one for Client Components.

### Server API client

**Location**: `lib/api/server-client.ts`

**Used in**: Server Components only

**Implementation**:

```tsx theme={null}
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

export async function serverApiClient<T>(endpoint: string, options?) {
  const cookieStore = await cookies();
  const accessToken = cookieStore.get('accessToken')?.value;
  
  const response = await fetch(`${API_URL}${endpoint}`, {
    headers: {
      'Content-Type': 'application/json',
      ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
    },
  });
  
  const data = await response.json();
  
  // Redirect to login on 401 (cannot refresh tokens in Server Components)
  if (response.status === 401) {
    redirect('/auth/login');
  }
  
  if (!response.ok) {
    throw new ServerApiError(data.error || 'API Error', response.status, data);
  }
  
  return data as T;
}
```

**Key characteristics**:

* Reads cookies directly via `next/headers`
* Cannot refresh tokens (can't modify cookies)
* Redirects to login on 401
* Throws errors for bad responses

**Example usage**:

```tsx theme={null}
// lib/api/auth.server.ts
export async function getCurrentUser() {
  return serverApiClient<GetCurrentUserResponse>('/api/auth/me', {
    redirectOnAuthError: true,
  });
}
```

### Client API client

**Location**: `lib/api/client.ts`

**Used in**: Client Components only

**Implementation**:

```tsx theme={null}
export async function apiClient<T>(endpoint: string, options?) {
  // Use /backend proxy to avoid CORS issues
  const apiUrl = typeof window !== 'undefined' 
    ? '/backend'  // Browser: use Next.js proxy
    : process.env.NEXT_PUBLIC_API_URL;  // Server: direct URL
  
  // Get access token via API route
  if (options?.requireAuth) {
    const accessToken = await getAccessToken();
    if (accessToken) {
      headers.Authorization = `Bearer ${accessToken}`;
    }
  }
  
  let response = await fetch(`${apiUrl}${endpoint}`, {
    ...options,
    credentials: 'include',
    headers,
  });
  
  let data = await response.json();
  
  // Automatic token refresh on 401
  if (response.status === 401 && !options?.skipAuthRetry) {
    const newAccessToken = await refreshAccessToken();
    
    if (newAccessToken) {
      // Retry request with new token
      response = await fetch(`${apiUrl}${endpoint}`, {
        ...options,
        headers: {
          ...headers,
          Authorization: `Bearer ${newAccessToken}`,
        },
      });
      
      data = await response.json();
    }
  }
  
  if (!response.ok) {
    throw new ApiError(data.error || 'API Error', response.status, data);
  }
  
  return data as T;
}
```

**Key characteristics**:

* Uses `/backend` proxy to avoid CORS
* Automatically refreshes tokens on 401
* Retries failed requests with new token
* Redirects to login if refresh fails

**Example usage**:

```tsx theme={null}
// lib/api/invoices.client.ts
export async function getInvoices(params: GetInvoicesParams) {
  const queryString = buildQueryString(params);
  return apiClient<GetInvoicesResponse>(`/api/invoices${queryString}`, {
    requireAuth: true,
  });
}
```

## Token refresh mechanism

Contafy implements automatic token refresh to keep users logged in.

### How it works

1. User makes authenticated request
2. Backend responds with 401 (token expired)
3. Client automatically calls `/api/auth/refresh`
4. Backend validates refresh token, returns new access token
5. Client retries original request with new token
6. User stays logged in without interruption

### Implementation details

**Refresh function** (`lib/api/client.ts:49-95`):

```tsx theme={null}
let isRefreshing = false;
let refreshPromise: Promise<string | null> | null = null;

async function refreshAccessToken(): Promise<string | null> {
  // Prevent multiple simultaneous refresh requests
  if (isRefreshing && refreshPromise) {
    return refreshPromise;
  }
  
  isRefreshing = true;
  refreshPromise = (async () => {
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include',
      });
      
      const data = await response.json();
      
      if (!response.ok) {
        // Refresh token expired, redirect to login
        if (response.status === 401 || data.redirect) {
          window.location.href = '/auth/login';
          return null;
        }
        return null;
      }
      
      return data.accessToken || null;
    } finally {
      isRefreshing = false;
      refreshPromise = null;
    }
  })();
  
  return refreshPromise;
}
```

**Key features**:

* Prevents duplicate refresh requests with flag
* Shares single refresh promise across requests
* Redirects to login if refresh fails
* Transparent to calling code

### Token storage

Tokens are stored in httpOnly cookies for security:

* **Access token**: Short-lived (15 minutes)
* **Refresh token**: Long-lived (7 days)

**Security benefits**:

* Cannot be accessed by JavaScript (XSS protection)
* Automatically sent with requests
* Secure flag in production (HTTPS only)

## API proxy configuration

To avoid CORS issues, Contafy proxies API requests through Next.js.

**Configuration** (`next.config.ts:5-12`):

```tsx theme={null}
const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/backend/:path*',
        destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`,
      },
    ];
  },
};
```

**How it works**:

1. Client requests `/backend/api/invoices`
2. Next.js rewrites to `http://localhost:3001/api/invoices`
3. Backend processes request
4. Next.js forwards response to client

**Benefits**:

* No CORS headers needed
* Simplified client code
* Works in all environments

## TanStack Query configuration

**Setup** (`components/providers/ReactQueryProvider.tsx:11-24`):

```tsx theme={null}
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,  // Don't refetch when window focused
      retry: 1,                      // Retry failed requests once
    },
  },
});
```

### Query keys

Query keys uniquely identify cached data:

```tsx theme={null}
// Simple key
queryKey: ['profiles']

// Key with parameters
queryKey: ['invoices', profileId, mes, año]

// Key with multiple parameters
queryKey: ['expenses', { profileId, mes, año, search }]
```

**Best practices**:

* Include all parameters that affect the data
* Use consistent ordering
* Use arrays for hierarchical keys

### Query functions

Query functions fetch the actual data:

```tsx theme={null}
queryFn: () => getInvoices({ profileId, mes, año })
```

**Requirements**:

* Must return a Promise
* Should use API client functions
* Can access query key parameters

### Cache invalidation

Invalidate queries when data changes:

```tsx theme={null}
import { useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();

// Invalidate specific query
await queryClient.invalidateQueries({ queryKey: ['invoices', profileId] });

// Invalidate all invoice queries
await queryClient.invalidateQueries({ queryKey: ['invoices'] });

// Invalidate and refetch immediately
await queryClient.invalidateQueries({ 
  queryKey: ['expenses'], 
  refetchType: 'active' 
});
```

### Mutations

Mutations modify server data:

```tsx theme={null}
import { useMutation, useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();

const deleteMutation = useMutation({
  mutationFn: (id: string) => deleteExpense(id),
  onSuccess: () => {
    // Invalidate and refetch expenses
    queryClient.invalidateQueries({ queryKey: ['expenses'] });
    toast.success('Gasto eliminado correctamente');
  },
  onError: (error) => {
    toast.error('Error al eliminar gasto');
  },
});

// Use mutation
deleteMutation.mutate(expenseId);
```

## Data flow examples

### Server Component data flow

1. User navigates to `/dashboard`
2. Server Component renders
3. Server fetches data via `serverApiClient`
4. Server renders HTML with data
5. Client receives fully rendered page
6. No loading state on client

### Client Component data flow

1. User clicks "Ver más" button
2. Client Component renders
3. Shows loading spinner
4. Client fetches data via `apiClient` + TanStack Query
5. Data cached by TanStack Query
6. Component re-renders with data
7. Cache reused on subsequent renders

## Error handling

### API errors

Both API clients throw `ApiError`/`ServerApiError`:

```tsx theme={null}
try {
  const data = await getInvoices();
} catch (error) {
  if (error instanceof ApiError) {
    console.error('API error:', error.status, error.message);
  }
}
```

### TanStack Query error handling

```tsx theme={null}
const { data, error, isError } = useQuery({
  queryKey: ['invoices'],
  queryFn: getInvoices,
});

if (isError) {
  return <ErrorMessage error={error} />;
}
```

## Performance optimizations

### Parallel data fetching

Fetch multiple resources in parallel:

```tsx theme={null}
const [data1, data2, data3] = await Promise.all([
  getMetrics(),
  getInvoices(),
  getExpenses(),
]);
```

### Request deduplication

TanStack Query automatically deduplicates identical requests:

```tsx theme={null}
// Both components use same query key
// Only one request sent, result shared
function Component1() {
  useQuery({ queryKey: ['invoices'], queryFn: getInvoices });
}

function Component2() {
  useQuery({ queryKey: ['invoices'], queryFn: getInvoices });
}
```

### Prefetching

Prefetch data before it's needed:

```tsx theme={null}
const queryClient = useQueryClient();

// Prefetch on hover
const handleMouseEnter = () => {
  queryClient.prefetchQuery({
    queryKey: ['invoice', invoiceId],
    queryFn: () => getInvoice(invoiceId),
  });
};
```

See [Architecture](/development/architecture) for how this fits into the overall system.
