Documentation Index
Fetch the complete documentation index at: https://mintlify.com/pakomercado0517/tresa-contafy-web/llms.txt
Use this file to discover all available pages before exploring further.
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:
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:
'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:
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:
// 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:
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:
// 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
- User makes authenticated request
- Backend responds with 401 (token expired)
- Client automatically calls
/api/auth/refresh
- Backend validates refresh token, returns new access token
- Client retries original request with new token
- User stays logged in without interruption
Implementation details
Refresh function (lib/api/client.ts:49-95):
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):
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/backend/:path*',
destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`,
},
];
},
};
How it works:
- Client requests
/backend/api/invoices
- Next.js rewrites to
http://localhost:3001/api/invoices
- Backend processes request
- 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):
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:
// 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:
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:
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:
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
- User navigates to
/dashboard
- Server Component renders
- Server fetches data via
serverApiClient
- Server renders HTML with data
- Client receives fully rendered page
- No loading state on client
Client Component data flow
- User clicks “Ver más” button
- Client Component renders
- Shows loading spinner
- Client fetches data via
apiClient + TanStack Query
- Data cached by TanStack Query
- Component re-renders with data
- Cache reused on subsequent renders
Error handling
API errors
Both API clients throw ApiError/ServerApiError:
try {
const data = await getInvoices();
} catch (error) {
if (error instanceof ApiError) {
console.error('API error:', error.status, error.message);
}
}
TanStack Query error handling
const { data, error, isError } = useQuery({
queryKey: ['invoices'],
queryFn: getInvoices,
});
if (isError) {
return <ErrorMessage error={error} />;
}
Parallel data fetching
Fetch multiple resources in parallel:
const [data1, data2, data3] = await Promise.all([
getMetrics(),
getInvoices(),
getExpenses(),
]);
Request deduplication
TanStack Query automatically deduplicates identical requests:
// 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:
const queryClient = useQueryClient();
// Prefetch on hover
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['invoice', invoiceId],
queryFn: () => getInvoice(invoiceId),
});
};
See Architecture for how this fits into the overall system.