Skip to main content

Overview

Villa Buena implements a clean separation between API communication and UI components through a layered architecture:
1

Axios Client

Configured HTTP client with base URL from environment variables
2

Service Layer

Domain-specific service modules that encapsulate API calls
3

Custom Hooks

React Query hooks that provide caching, refetching, and state management
4

Components

UI components consume hooks without direct API knowledge

Axios Configuration

The base API client is configured in src/services/api.js:3-5:
import axios from "axios";

export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
});

Environment Variables

The API URL is loaded from Vite environment variables, allowing different configurations per environment:
# .env.development
VITE_API_URL=https://api.escuelajs.co/api/v1

# .env.production
VITE_API_URL=https://production-api.example.com/api/v1
Vite environment variables must be prefixed with VITE_ to be exposed to client-side code. Access them via import.meta.env.VITE_*.

Axios Instance Benefits

Centralized Config

All requests automatically use the base URL

Interceptors

Easily add auth tokens, error handling, or logging

Custom Defaults

Set timeout, headers, and other defaults

Testability

Mock the instance for testing without affecting global axios

Service Layer

The service layer abstracts API communication into reusable modules. The productService (src/services/productService.js) demonstrates this pattern:
import { api } from "./api";

export const productService = {
  getAll: async () => {
    const { data } = await api.get("/products?limit=200");
    return data;
  },
  
  getById: async (id) => {
    const { data } = await api.get(`/products/${id}`);
    return data;
  },
  
  getByCategory: async (category) => {
    const { data } = await api.get(`/products/category/${category}`);
    return data;
  },
};

Service Layer Benefits

1

Single Responsibility

Each service module handles one domain (products, users, orders, etc.)
2

API Abstraction

Components don’t need to know about endpoints, HTTP methods, or response structures
3

Reusability

Services can be used across multiple hooks and components
4

Testability

Easy to mock service methods in component tests

API Endpoints

The product service exposes three methods:
MethodEndpointDescription
getAll()GET /products?limit=200Fetch all products (max 200)
getById(id)GET /products/:idFetch single product by ID
getByCategory(category)GET /products/category/:categoryFetch products in category
The getAll method includes a query parameter ?limit=200 to fetch a larger dataset from the API.

React Query Integration

React Query (TanStack Query v5) provides powerful server state management with automatic caching, background refetching, and optimistic updates.

Query Provider Setup

The QueryClient is configured in src/app/providers/QueryProvider.jsx:1-8:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

export const QueryProvider = ({ children }) => {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};
This provider wraps the entire application in main.jsx:14, making React Query available to all components.

Custom React Query Hooks

The useProducts hook module (src/hooks/useProducts.js) combines React Query with the product service:
import { useQuery } from "@tanstack/react-query";
import { productService } from "../services/productService";

export const useProducts = () => {
  return useQuery({
    queryKey: ["products"],
    queryFn: productService.getAll,
  });
};

export const useProduct = (id) => {
  return useQuery({
    queryKey: ["product", id],
    queryFn: () => productService.getById(id),
    enabled: !!id,
  });
};

export const useProductsByCategory = (category) => {
  return useQuery({
    queryKey: ["products", "category", category],
    queryFn: () => productService.getByCategory(category),
    enabled: !!category,
  });
};

Query Key Strategy

React Query uses query keys for caching and invalidation:
// Simple key for all products
queryKey: ["products"]

// Hierarchical key for single product
queryKey: ["product", id]

// Nested key for category filtering
queryKey: ["products", "category", category]
Query keys should be arrays that uniquely identify the data. Include all variables that affect the query (like id or category) in the key.

Conditional Fetching

Both useProduct and useProductsByCategory use the enabled option to prevent fetching until required parameters are available:
enabled: !!id  // Only fetch when id is truthy
enabled: !!category  // Only fetch when category is truthy
This prevents unnecessary API calls and errors from invalid parameters.

Using Hooks in Components

Fetching All Products

import { useProducts } from '../hooks/useProducts';

function ProductList() {
  const { data: products, isLoading, error } = useProducts();
  
  if (isLoading) return <div>Loading products...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Fetching Single Product

import { useParams } from 'react-router-dom';
import { useProduct } from '../hooks/useProducts';

function ProductDetail() {
  const { id } = useParams();
  const { data: product, isLoading, error } = useProduct(id);
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading product</div>;
  
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>
    </div>
  );
}

React Query Return Values

PropertyTypeDescription
dataanyThe fetched data (undefined until loaded)
isLoadingbooleanTrue during initial fetch
isFetchingbooleanTrue during any fetch (including background)
errorError | nullError object if fetch failed
isErrorbooleanTrue if query is in error state
refetchfunctionManually trigger a refetch

Caching and Refetching

React Query automatically manages cache and refetching:
1

Automatic Caching

Query results are cached by their query key. Subsequent requests return cached data instantly.
2

Background Refetching

When the window regains focus, React Query refetches to ensure fresh data.
3

Stale Time

Configure how long data is considered fresh before triggering background updates.
4

Cache Time

Control how long unused data stays in cache before garbage collection.

Configuring Cache Behavior

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
  },
});

Error Handling

Handle errors gracefully in components:
function ProductList() {
  const { data, isLoading, error, isError } = useProducts();
  
  if (isLoading) {
    return <LoadingSpinner />;
  }
  
  if (isError) {
    return (
      <div className="alert alert-danger">
        <h4>Failed to load products</h4>
        <p>{error.message}</p>
        <button onClick={() => refetch()}>Retry</button>
      </div>
    );
  }
  
  return <ProductGrid products={data} />;
}
React Query provides retry options to automatically retry failed requests with exponential backoff.

Mutations (Future)

While the current codebase only implements queries, React Query also supports mutations for POST/PUT/DELETE operations:
import { useMutation, useQueryClient } from '@tanstack/react-query';

function useCreateProduct() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: (newProduct) => productService.create(newProduct),
    onSuccess: () => {
      // Invalidate and refetch products
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });
}

Architecture Diagram

Best Practices

1

Keep Services Pure

Service methods should only handle HTTP communication, not state management or business logic.
2

Use Query Keys Wisely

Include all variables that affect the query result in the query key for proper cache invalidation.
3

Handle Loading States

Always provide feedback during loading and error states for better UX.
4

Leverage Cache

Trust React Query’s caching to reduce unnecessary API calls and improve performance.
5

Centralize Configuration

Keep API configuration (base URL, timeouts, headers) in the axios instance.

Testing

Mocking Services

// __mocks__/productService.js
export const productService = {
  getAll: jest.fn(() => Promise.resolve(mockProducts)),
  getById: jest.fn((id) => Promise.resolve(mockProducts[0])),
};

Testing with React Query

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';

const createTestQueryClient = () => new QueryClient({
  defaultOptions: {
    queries: { retry: false },
  },
});

test('renders product list', async () => {
  const queryClient = createTestQueryClient();
  
  render(
    <QueryClientProvider client={queryClient}>
      <ProductList />
    </QueryClientProvider>
  );
  
  expect(await screen.findByText('Product Name')).toBeInTheDocument();
});
Disable retries in test environments to make tests faster and more predictable.

Build docs developers (and LLMs) love