Skip to content

πŸ”§ API Utilities - Refactoring ​

🎯 Obiettivo ​

Centralizzare le utility API (apiRequest, ApiError, helper functions) in un file riusabile per tutte le API del progetto, evitando duplicazione di codice e garantendo configurazione consistente.

βœ… Struttura File ​

1. app/lib/api.ts - Utility Generiche ⭐ ​

File centrale con funzioni riusabili per tutte le API:

typescript
// Exports principali:
- apiRequest<T>()      // Wrapper fetch con Zod validation
- ApiError             // Classe errori tipizzati
- getDefaultErrorMessage()  // Messaggi user-friendly
- buildQueryString()   // Helper per query params

Configurazione centralizzata:

  • βœ… Headers comuni: Content-Type, Daisy-Erp
  • βœ… Credentials: include (cookies automatici)
  • βœ… Gestione errori standardizzata
  • βœ… Validazione Zod integrata

2. app/api/auth/auth.api.ts - API Autenticazione ​

API specifica per autenticazione che importa le utility generiche:

typescript
import { apiRequest, ApiError } from "@/lib/api";

// Usa apiRequest() per tutte le chiamate
export async function login(credentials: LoginRequest) {
    return apiRequest<LoginResponse>(
        API_BASE_URL,    // Base URL specifica
        "/login",        // Endpoint
        { method: "POST", body: JSON.stringify(credentials) },
        LoginResponseSchema  // Schema Zod
    );
}

3. app/examples/products.api.example.ts - Esempio ​

Esempio completo di come creare nuove API riusando le utility:

typescript
import { apiRequest, ApiError, buildQueryString } from "@/lib/api";

const PRODUCTS_API_BASE_URL = "https://api.local.daisy/erp/products";

export async function getProducts(params?: { page?: number }) {
    const queryString = buildQueryString(params || {});

    return apiRequest<ProductsListResponse>(
        PRODUCTS_API_BASE_URL,
        `/list${queryString}`,
        { method: "GET" },
        ProductsListResponseSchema
    );
}

πŸ“Š Prima vs Dopo ​

Prima (❌ Codice Duplicato) ​

app/api/auth.api.ts:
  - class ApiError { ... }              // 10 righe
  - function apiRequest() { ... }       // 40 righe
  - function getDefaultErrorMessage() { ... }  // 10 righe
  - Total: 60 righe duplicate per ogni file API

app/api/products.api.ts:
  - class ApiError { ... }              // 10 righe (DUPLICATO!)
  - function apiRequest() { ... }       // 40 righe (DUPLICATO!)
  - function getDefaultErrorMessage() { ... }  // 10 righe (DUPLICATO!)
  - Total: 60 righe duplicate

app/api/orders.api.ts:
  - class ApiError { ... }              // 10 righe (DUPLICATO!)
  - ...

❌ Problemi:
- Duplicazione codice
- Inconsistenze tra file
- Difficile manutenzione
- Possibili bug se si modifica solo un file

Dopo (βœ… Centralizzato) ​

app/lib/api.ts:
  - class ApiError { ... }              // 10 righe (UNICA VOLTA)
  - function apiRequest() { ... }       // 40 righe (UNICA VOLTA)
  - function getDefaultErrorMessage() { ... }  // 10 righe (UNICA VOLTA)
  - function buildQueryString() { ... } // 10 righe (BONUS!)
  - Total: 70 righe centrali

app/api/auth/auth.api.ts:
  - import { apiRequest, ApiError } from "@/lib/api";
  - export async function login() { ... }
  - export async function logout() { ... }
  - Total: 120 righe (solo logica business)

app/api/products/products.api.ts:
  - import { apiRequest, ApiError } from "@/lib/api";
  - export async function getProducts() { ... }
  - Total: 80 righe (solo logica business)

βœ… Vantaggi:
- Zero duplicazione
- Configurazione consistente
- Manutenzione centralizzata
- Un bugfix corregge tutte le API

πŸ”§ Signature apiRequest ​

typescript
async function apiRequest<T>(
    baseUrl: string,        // URL base API (es. "https://api.local.daisy/erp/auth")
    endpoint: string,       // Endpoint relativo (es. "/login")
    options: RequestInit,   // Opzioni fetch (method, body, headers)
    schema: z.ZodType<T>   // Schema Zod per validazione
): Promise<T>

Parametri ​

ParametroTipoDescrizioneEsempio
baseUrlstringURL base dell'API"https://api.local.daisy/erp/auth"
endpointstringEndpoint relativo"/login" o "/products/123"
optionsRequestInitOpzioni fetch standard{ method: "POST", body: "..." }
schemaZodType<T>Schema Zod per validare rispostaLoginResponseSchema

Headers Automatici ​

typescript
// Questi header sono aggiunti AUTOMATICAMENTE a TUTTE le richieste:
headers: {
    "Content-Type": "application/json",
    "Daisy-Erp": "frontend",
    ...options.headers,  // Puoi aggiungere altri header
}

Credentials ​

typescript
credentials: "include"  // βœ… Cookies automatici in TUTTE le richieste

πŸ“ Come Creare Nuove API ​

Step 1: Definisci Base URL e Schemi ​

typescript
import { apiRequest, ApiError } from "../lib/api";
import { createApiResponseSchema } from "../schemas/api.schema";

// 1. Base URL della tua API
const MY_API_BASE_URL = "https://api.local.daisy/erp/my_module";

// 2. Schemi Zod
const ItemSchema = z.object({
    id: z.number(),
    name: z.string(),
});

const ItemResponseSchema = createApiResponseSchema(ItemSchema);
export type ItemResponse = z.infer<typeof ItemResponseSchema>;

Step 2: Crea Funzioni API ​

typescript
// GET
export async function getItem(id: number): Promise<ItemResponse> {
    return apiRequest<ItemResponse>(
        MY_API_BASE_URL,
        `/${id}`,
        { method: "GET" },
        ItemResponseSchema
    );
}

// POST
export async function createItem(data: ItemCreate): Promise<ItemResponse> {
    const validatedData = ItemCreateSchema.parse(data);

    return apiRequest<ItemResponse>(
        MY_API_BASE_URL,
        "/",
        {
            method: "POST",
            body: JSON.stringify(validatedData),
        },
        ItemResponseSchema
    );
}

// PUT
export async function updateItem(id: number, data: Partial<ItemCreate>): Promise<ItemResponse> {
    return apiRequest<ItemResponse>(
        MY_API_BASE_URL,
        `/${id}`,
        {
            method: "PUT",
            body: JSON.stringify(data),
        },
        ItemResponseSchema
    );
}

// DELETE
export async function deleteItem(id: number): Promise<ItemResponse> {
    return apiRequest<ItemResponse>(
        MY_API_BASE_URL,
        `/${id}`,
        { method: "DELETE" },
        ItemResponseSchema
    );
}

Step 3: Usa le API nei Componenti ​

typescript
import { getItem, createItem, ApiError } from "../api/my-module.api";

async function handleLoad() {
    try {
        const result = await getItem(123);

        if (result.data) {
            console.log("Item:", result.data.name);
        }

    } catch (error) {
        if (error instanceof ApiError) {
            if (error.status === 401) {
                // Redirect al login
                window.location.href = "/";
            } else {
                alert(error.message); // Messaggio user-friendly
            }
        }
    }
}

🎨 FunzionalitΓ  Helper ​

buildQueryString() ​

Costruisce query string da oggetto:

typescript
import { buildQueryString } from "../lib/api";

const params = { page: 1, limit: 10, search: "laptop" };
const query = buildQueryString(params);
// β†’ "?page=1&limit=10&search=laptop"

// Uso
const response = await apiRequest(
    BASE_URL,
    `/list${query}`,  // "/list?page=1&limit=10&search=laptop"
    { method: "GET" },
    schema
);

getDefaultErrorMessage() ​

Messaggi user-friendly per status HTTP:

typescript
import { getDefaultErrorMessage } from "../lib/api";

console.log(getDefaultErrorMessage(401)); // "Credenziali non valide o sessione scaduta"
console.log(getDefaultErrorMessage(500)); // "Errore del server"
console.log(getDefaultErrorMessage(999)); // "Errore nella richiesta" (default)

Status supportati:

  • 400: "Richiesta non valida"
  • 401: "Credenziali non valide o sessione scaduta"
  • 403: "Accesso negato"
  • 404: "Risorsa non trovata"
  • 429: "Troppi tentativi, riprova piΓΉ tardi"
  • 500: "Errore del server"
  • 502: "Gateway non disponibile"
  • 503: "Servizio non disponibile"

Puoi estendere l'oggetto messages in api.ts per aggiungere altri status.

πŸ›‘οΈ Gestione Errori ​

ApiError Class ​

typescript
class ApiError extends Error {
    constructor(
        message: string,    // Messaggio user-friendly
        public status: number,     // HTTP status code
        public context?: string    // Contesto errore dal server
    )
}

Gestione automatica errori 401 (Unauthorized) ​

A partire dalla nuova implementazione, quando una chiamata API restituisce un errore 401 e l'utente Γ¨ autenticato, il sistema gestisce automaticamente la situazione:

  1. Pulizia dello store: Viene eseguito il logout automatico
  2. Alert utente: Viene mostrato un messaggio di errore
  3. Redirect al login: L'utente viene reindirizzato alla pagina di login

Questo comportamento Γ¨ integrato in apiRequest() e non richiede gestione manuale:

typescript
// In app/lib/api.ts
if (response.status === 401) {
    const isAuthenticated = useAuthStore.getState().isAuthenticated;
    if (isAuthenticated) {
        // Pulisce lo store
        useAuthStore.getState().logout();

        // Mostra alert e redirige al login quando viene chiuso
        alert(errorMessage);
        window.location.href = "/";
    }
}

Uso ​

typescript
try {
    await login({ username, password });
} catch (error) {
    if (error instanceof ApiError) {
        console.log("Messaggio:", error.message);  // "Credenziali non valide"
        console.log("Status:", error.status);      // 401
        console.log("Context:", error.context);    // "auth"

        // Gestione specifica per status
        switch (error.status) {
            case 401:
                // Per utenti autenticati, il redirect Γ¨ automatico
                // Per utenti non autenticati, gestisci qui
                break;
            case 403:
                // Accesso negato
                break;
            case 500:
                // Errore server
                break;
        }
    }
}

πŸ§ͺ Testing ​

I test rimangono invariati! Importano ApiError da auth.api.ts che ora lo ri-esporta da api.ts:

typescript
// tests/auth.api.test.ts
import { login, ApiError } from "../app/api/auth.api";

it("dovrebbe lanciare ApiError per login fallito", async () => {
    mockFetch.mockResolvedValueOnce({
        json: async () => ({ success: false, message: null, data: null }),
        status: 401,
    });

    await expect(
        login({ username: "test", password: "wrong" })
    ).rejects.toThrow(ApiError);
});

// βœ… Test passano: 28/28

πŸ“‚ Struttura File Finale ​

app/
β”œβ”€β”€ lib/
β”‚   └── api.ts                 ⭐ Utility generiche riusabili
β”œβ”€β”€ api/
β”‚   β”œβ”€β”€ auth.api.ts           βœ… USA le utility
β”‚   β”œβ”€β”€ products.api.ts       βœ… USA le utility (futuro)
β”‚   └── orders.api.ts         βœ… USA le utility (futuro)
β”œβ”€β”€ examples/
β”‚   └── products.api.example.ts  πŸ“š Esempio completo
└── schemas/
    └── api.schema.ts         πŸ“ Schemi base riusabili

πŸ“ Best Practices ​

βœ… DO ​

  • Importa sempre apiRequest e ApiError da app/lib/api.ts
  • Usa createApiResponseSchema() per schemi consistenti
  • Definisci una costante API_BASE_URL per ogni modulo API
  • Valida input con Zod prima di chiamare apiRequest()
  • Gestisci ApiError con instanceof nei componenti
  • Re-esporta ApiError dai tuoi file API per comoditΓ 

❌ DON'T ​

  • Non duplicare apiRequest o ApiError in altri file
  • Non modificare headers/credentials in apiRequest() custom
  • Non fare fetch direttamente senza usare apiRequest()
  • Non dimenticare di passare lo schema Zod per validazione
  • Non ignorare gli errori ApiError

πŸŽ‰ Risultato ​

βœ… Utility centralizzate in app/lib/api.ts βœ… Zero duplicazione di codice βœ… Configurazione consistente (headers, credentials) βœ… Facile manutenzione (un posto solo da modificare) βœ… Riusabile per tutte le API del progetto βœ… Test passanti (28/28) βœ… Esempio completo per nuove API


Ultima modifica: 14 Ottobre 2025 File coinvolti:

  • app/lib/api.ts (nuovo - utility generiche)
  • app/api/auth.api.ts (refactored - usa utility)
  • app/examples/products.api.example.ts (nuovo - esempio)
  • tests/auth.api.test.ts (invariato - funziona senza modifiche)

Risparmio codice: ~100 righe duplicate eliminate per ogni nuovo file API!

Documentazione Elerama Frontend