Skip to main content

API Integration

The Iquea frontend communicates with the backend through a type-safe REST API client located in src/api/. All API modules use a shared client with automatic JWT authentication.

API Architecture

Base Client

Shared HTTP client with auth headers

Service Modules

Domain-specific API functions

Type Safety

TypeScript interfaces for all requests/responses

JWT Auth

Automatic token injection from localStorage

Base API Client

The foundation of all API calls is the apiFetch function in client.ts:
src/api/client.ts
const BASE_URL = 'http://localhost:8080/api';

function getToken(): string | null {
    return localStorage.getItem('token');
}

function authHeaders(): HeadersInit {
    const token = getToken();
    return {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
    };
}

export async function apiFetch<T>(
    path: string,
    options: RequestInit = {}
): Promise<T> {
    const res = await fetch(`${BASE_URL}${path}`, {
        ...options,
        headers: {
            ...authHeaders(),
            ...(options.headers ?? {}),
        },
    });

    if (!res.ok) {
        const error = await res.json().catch(() => ({ message: 'Error desconocido' }));
        throw new Error(error.message ?? `Error ${res.status}`);
    }

    return res.json() as Promise<T>;
}

Key Features

The client automatically includes JWT tokens from localStorage:
function authHeaders(): HeadersInit {
    const token = getToken();
    return {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
    };
}
If a token exists, it’s added as Authorization: Bearer <token>.

Authentication API

Handles user login and registration.
src/api/auth.ts
import { apiFetch } from './client';
import type { LoginDTO, RegistroDTO, TokenDTO } from '../types';

export function login(data: LoginDTO): Promise<TokenDTO> {
    return apiFetch<TokenDTO>('/auth/login', {
        method: 'POST',
        body: JSON.stringify(data),
    });
}

export function registro(data: RegistroDTO): Promise<TokenDTO> {
    return apiFetch<TokenDTO>('/auth/registro', {
        method: 'POST',
        body: JSON.stringify(data),
    });
}

Type Definitions

src/types/index.ts
export interface LoginDTO {
    email: string;
    password: string;
}

export interface RegistroDTO {
    username: string;
    nombre: string;
    apellidos: string;
    email: { value: string };
    password: string;
    fecha_nacimiento: string; // "YYYY-MM-DD"
    direccion_envio?: string;
}

export interface TokenDTO {
    token: string;
}

Usage Example

src/pages/Login.tsx
import { login } from '../api/auth';
import { useAuth } from '../context/AuthContext';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

function Login() {
    const { setToken } = useAuth();
    const navigate = useNavigate();
    const [credentials, setCredentials] = useState({ email: '', password: '' });
    const [error, setError] = useState('');

    async function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        try {
            const { token } = await login(credentials);
            setToken(token);
            navigate('/');
        } catch (err) {
            setError(err instanceof Error ? err.message : 'Login failed');
        }
    }

    // Render form...
}

Products API

Fetches product data with various filtering options.
src/api/productos.ts
import { apiFetch } from './client';
import type { Producto } from '../types';

export function getProductos(): Promise<Producto[]> {
    return apiFetch<Producto[]>('/productos');
}

export function getProducto(id: number): Promise<Producto> {
    return apiFetch<Producto>(`/productos/${id}`);
}

export function getDestacados(): Promise<Producto[]> {
    return apiFetch<Producto[]>('/productos/destacados');
}

export function buscarProductos(texto: string): Promise<Producto[]> {
    return apiFetch<Producto[]>(`/productos/buscar?nombre=${encodeURIComponent(texto)}`);
}

export function getProductosPorCategoria(categoriaId: number): Promise<Producto[]> {
    return apiFetch<Producto[]>(`/productos/categoria/${categoriaId}`);
}

export function getProductosPorPrecio(min: number, max: number): Promise<Producto[]> {
    return apiFetch<Producto[]>(`/productos/precio?min=${min}&max=${max}`);
}

API Endpoints

FunctionMethodEndpointDescription
getProductos()GET/productosGet all products
getProducto(id)GET/productos/{id}Get single product
getDestacados()GET/productos/destacadosGet featured products
buscarProductos(texto)GET/productos/buscar?nombre={texto}Search by name
getProductosPorCategoria(id)GET/productos/categoria/{id}Filter by category
getProductosPorPrecio(min, max)GET/productos/precio?min={min}&max={max}Filter by price range

Product Type

src/types/index.ts
export interface Producto {
    producto_id: number;
    sku: string;
    nombre: string;
    descripcion: string;
    precioCantidad: number;
    precioMoneda: string;
    dimensionesAlto: number;
    dimensionesAncho: number;
    dimensionesProfundo: number;
    es_destacado: boolean;
    stock: number;
    imagen_url: string;
    categoria: CategoriaResumen;
}

export interface CategoriaResumen {
    categoria_id: number;
    nombre: string;
    slug: string;
}

Usage Example

src/pages/ProductList.tsx
import { getProductos, buscarProductos } from '../api/productos';
import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import ProductoCard from '../components/ProductoCard';

function ProductList() {
    const [productos, setProductos] = useState<Producto[]>([]);
    const [loading, setLoading] = useState(true);
    const [searchParams] = useSearchParams();
    const searchQuery = searchParams.get('q');

    useEffect(() => {
        async function fetchData() {
            setLoading(true);
            try {
                const data = searchQuery
                    ? await buscarProductos(searchQuery)
                    : await getProductos();
                setProductos(data);
            } catch (error) {
                console.error('Failed to fetch products:', error);
            } finally {
                setLoading(false);
            }
        }
        fetchData();
    }, [searchQuery]);

    if (loading) return <p>Loading...</p>;

    return (
        <div className="product-grid">
            {productos.map((producto) => (
                <ProductoCard key={producto.producto_id} producto={producto} />
            ))}
        </div>
    );
}

Categories API

Fetches product categories.
src/api/categorias.ts
import { apiFetch } from './client';
import type { CategoriaResumen } from '../types';

export function getCategorias(): Promise<CategoriaResumen[]> {
    return apiFetch<CategoriaResumen[]>('/categorias');
}

Orders API

Handles order creation and management (requires authentication).
src/api/pedidos.ts
import { apiFetch } from './client';
import type { Pedido, DetallePedido } from '../types';

export function getPedidos(): Promise<Pedido[]> {
    return apiFetch<Pedido[]>('/pedidos');
}

export function getPedido(id: number): Promise<Pedido> {
    return apiFetch<Pedido>(`/pedidos/${id}`);
}

export function crearPedido(usuarioId: number): Promise<Pedido> {
    return apiFetch<Pedido>('/pedidos', {
        method: 'POST',
        body: JSON.stringify({ usuario_id: usuarioId }),
    });
}

export function getDetallesPedido(pedidoId: number): Promise<DetallePedido[]> {
    return apiFetch<DetallePedido[]>(`/pedidos/${pedidoId}/detalles`);
}

export function anhadirDetalle(
    pedidoId: number,
    productoId: number,
    cantidad: number
): Promise<DetallePedido> {
    return apiFetch<DetallePedido>(`/pedidos/${pedidoId}/detalles`, {
        method: 'POST',
        body: JSON.stringify({ producto_id: productoId, cantidad }),
    });
}

export function eliminarDetalle(detalleId: number): Promise<void> {
    return apiFetch<void>(`/detalles/${detalleId}`, { method: 'DELETE' });
}

export function actualizarCantidad(detalleId: number, cantidad: number): Promise<DetallePedido> {
    return apiFetch<DetallePedido>(`/detalles/${detalleId}/cantidad?cantidad=${cantidad}`, {
        method: 'PUT',
    });
}

Order Types

src/types/index.ts
export type EstadoPedido = 'PENDIENTE' | 'CONFIRMADO' | 'ENVIADO' | 'ENTREGADO' | 'CANCELADO';

export interface ProductoResumen {
    producto_id: number;
    nombre: string;
    precioCantidad: number;
    imagen_url: string;
}

export interface DetallePedido {
    detalle_id: number;
    producto: ProductoResumen;
    cantidad: number;
    precioUnitario: number;
    subtotal: number;
}

export interface Pedido {
    pedido_id: number;
    referencia: string;
    fecha_creacion: string;
    estado: EstadoPedido;
    total: number;
    detalles: DetallePedido[];
}

Checkout Flow Example

src/pages/Cart.tsx
import { crearPedido, anhadirDetalle } from '../api/pedidos';
import { useCart } from '../context/CartContext';
import { useAuth } from '../context/AuthContext';
import { useState } from 'react';

function Cart() {
    const { cart, clearCart } = useCart();
    const { email } = useAuth();  // In real app, get user ID
    const [loading, setLoading] = useState(false);

    async function handleCheckout() {
        setLoading(true);
        try {
            // 1. Create order
            const pedido = await crearPedido(1);  // Use actual user ID

            // 2. Add all cart items to order
            for (const item of cart) {
                await anhadirDetalle(
                    pedido.pedido_id,
                    item.producto.producto_id,
                    item.cantidad
                );
            }

            // 3. Clear cart and show success
            clearCart();
            alert('Order placed successfully!');
        } catch (error) {
            console.error('Checkout failed:', error);
            alert('Failed to place order');
        } finally {
            setLoading(false);
        }
    }

    // Render cart UI...
}

Error Handling

All API functions can throw errors that should be caught:
import { getProducto } from '../api/productos';

async function loadProduct(id: number) {
    try {
        const producto = await getProducto(id);
        setProducto(producto);
    } catch (error) {
        if (error instanceof Error) {
            setError(error.message);
        } else {
            setError('Unknown error occurred');
        }
    }
}

JWT Token Flow

The authentication flow works as follows:

Token Storage

// Login flow
const { token } = await login(credentials);
setToken(token);  // Saves to localStorage via AuthContext

// API client reads token
function getToken(): string | null {
    return localStorage.getItem('token');
}

// Automatically included in headers
function authHeaders(): HeadersInit {
    const token = getToken();
    return {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
    };
}

API Best Practices

Specify the expected return type for type safety:
// Good
const productos = await apiFetch<Producto[]>('/productos');

// Bad - loses type information
const productos = await apiFetch('/productos');
Always encode user input in URLs:
// Good
const query = encodeURIComponent(searchText);
await apiFetch(`/productos/buscar?nombre=${query}`);

// Bad - vulnerable to injection
await apiFetch(`/productos/buscar?nombre=${searchText}`);
Manage loading, error, and success states:
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
Use cleanup functions in useEffect:
useEffect(() => {
    let cancelled = false;
    
    async function fetchData() {
        const data = await getProductos();
        if (!cancelled) {
            setProductos(data);
        }
    }
    fetchData();
    
    return () => { cancelled = true; };
}, []);

Configuration

The base API URL is hardcoded in client.ts:
const BASE_URL = 'http://localhost:8080/api';
Production Configuration: Update BASE_URL for production deployments. Consider using environment variables:
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api';

State Management

See how AuthContext manages JWT tokens

Backend API

Full backend API endpoint documentation

Build docs developers (and LLMs) love