π§ 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:
// Exports principali:
- apiRequest<T>() // Wrapper fetch con Zod validation
- ApiError // Classe errori tipizzati
- getDefaultErrorMessage() // Messaggi user-friendly
- buildQueryString() // Helper per query paramsConfigurazione 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:
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:
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 fileDopo (β 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 β
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 β
| Parametro | Tipo | Descrizione | Esempio |
|---|---|---|---|
baseUrl | string | URL base dell'API | "https://api.local.daisy/erp/auth" |
endpoint | string | Endpoint relativo | "/login" o "/products/123" |
options | RequestInit | Opzioni fetch standard | { method: "POST", body: "..." } |
schema | ZodType<T> | Schema Zod per validare risposta | LoginResponseSchema |
Headers Automatici β
// Questi header sono aggiunti AUTOMATICAMENTE a TUTTE le richieste:
headers: {
"Content-Type": "application/json",
"Daisy-Erp": "frontend",
...options.headers, // Puoi aggiungere altri header
}Credentials β
credentials: "include" // β
Cookies automatici in TUTTE le richiesteπ Come Creare Nuove API β
Step 1: Definisci Base URL e Schemi β
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 β
// 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 β
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:
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:
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 β
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:
- Pulizia dello store: Viene eseguito il logout automatico
- Alert utente: Viene mostrato un messaggio di errore
- Redirect al login: L'utente viene reindirizzato alla pagina di login
Questo comportamento Γ¨ integrato in apiRequest() e non richiede gestione manuale:
// 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 β
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:
// 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
apiRequesteApiErrordaapp/lib/api.ts - Usa
createApiResponseSchema()per schemi consistenti - Definisci una costante
API_BASE_URLper ogni modulo API - Valida input con Zod prima di chiamare
apiRequest() - Gestisci
ApiErrorconinstanceofnei componenti - Re-esporta
ApiErrordai tuoi file API per comoditΓ
β DON'T β
- Non duplicare
apiRequestoApiErrorin 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!