npm package: @jysperu/helper-fetcher
Description: Librería moderna TypeScript para peticiones HTTP con autenticación (Bearer/Token), headers personalizados, timeout configurable y detección automática de URL base
Author: JYS Perú
Latest version: 1.1.2

# @jysperu/helper-fetcher

[![npm version](https://badge.fury.io/js/@jysperu%2Fhelper-fetcher.svg)](https://badge.fury.io/js/@jysperu%2Fhelper-fetcher)
[![npm downloads](https://img.shields.io/npm/dm/@jysperu/helper-fetcher)](https://www.npmjs.com/package/@jysperu/helper-fetcher)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@jysperu/helper-fetcher)](https://bundlephobia.com/package/@jysperu/helper-fetcher)

Librería moderna **TypeScript** para peticiones HTTP con **autenticación** (Bearer/Token), headers personalizados, timeout configurable y detección automática de URL base.

## 🚀 Características

- 🔧 **TypeScript** completamente tipado con genéricos
- 🔑 **Autenticación integrada** - Bearer Token y headers personalizados
- ⚡ **API moderna** basada en fetch con timeout
- 🛡️ **Manejo de errores** robusto con soporte para respuestas no-JSON
- 🎯 **Métodos HTTP** completos: GET, POST, PUT, DELETE, PATCH
- 🆕 **Métodos directos** - `fetcher.get()`, `fetcher.post()`, etc. (v1.0.2)
- 📤 **Upload/Download** con progreso - `fetcher.upload()`, `fetcher.download()` (v1.1.0)
- 📦 **Mínimas dependencias** - Solo `form-data` para Node.js
- 🔄 **Cache control** granular y personalizable
- 🌐 **Detección automática** del tag `<base>` HTML o fallback inteligente
- ⚡ **Singleton pattern** con instancia global
- 🔍 **Respuestas flexibles** - Maneja JSON, texto plano y HTML (v1.0.5)
- 🌍 **Multiplataforma** - Node.js y navegador con mismo código

## 📦 Instalación

```bash
npm install @jysperu/helper-fetcher
```

## ⚡ Inicio Rápido

```typescript
import { fetcher } from "@jysperu/helper-fetcher";

// Configuración global con autenticación
fetcher.configure({
  base: "https://api.ejemplo.com",
  timeout: 5000,
  bearerToken: "tu-jwt-token", // Automáticamente añade Authorization: Bearer
  headers: {
    "X-Custom-Header": "valor-personalizado",
  },
});

import { api, api_get, api_post } from "@jysperu/helper-fetcher";

// GET request con tipado
interface User {
  id: number;
  name: string;
  email: string;
}

const usuarios = await api_get<User[]>("/usuarios");
if (usuarios.success) {
  console.log("Usuarios:", usuarios.data);
} else {
  console.error("Error:", usuarios.log);
}

// POST request
const nuevoUsuario = await api_post<User>("/usuarios", {
  name: "Juan Pérez",
  email: "juan@ejemplo.com",
});

// Usando la función principal con cache control
const result = await api<User[]>("/usuarios", {}, false, "GET"); // Sin cache

// 🆕 Nuevos métodos HTTP directos (v1.0.2)
const users = await fetcher.get<User[]>("/usuarios"); // GET
const newUser = await fetcher.post<User>("/usuarios", { name: "Ana" }); // POST
const updated = await fetcher.put<User>("/usuarios/1", { name: "Ana García" }); // PUT
await fetcher.delete("/usuarios/1"); // DELETE
const patched = await fetcher.patch<User>("/usuarios/1", { email: "ana@nuevo.com" }); // PATCH

// Método genérico con detección automática
const data = await fetcher.request<User[]>("/usuarios", "GET", { active: true });
```

## 📚 API Reference

### 🏭 Clase Fetcher

La clase principal que maneja todas las peticiones HTTP.

```typescript
class Fetcher {
  static instance: Fetcher; // Instancia singleton

  constructor(config?: Partial<RequestConfig>);
  configure(config: Partial<RequestConfig>): void;
  buildUrl(endpoint: string, params?: ApiParams, cachecode?: string | number | boolean | null): string;
  fetch<T>(url: string, options: RequestInit, timeout?: number): Promise<ApiResponse<T>>;
  fetchWithParams<T>(
    endpoint: string,
    method?: string,
    params?: ApiParams,
    cachecode?: string | number | boolean | null,
    timeout?: number
  ): Promise<ApiResponse<T>>;
  fetchWithBody<T>(endpoint: string, method?: string, body?: ApiBody, timeout?: number): Promise<ApiResponse<T>>;

  // Métodos HTTP directos (nuevos en v1.0.2)
  request<T>(
    endpoint: string,
    method?: HttpMethod,
    data?: ApiParams | ApiBody,
    cachecode?: string | number | boolean | null
  ): Promise<ApiResponse<T>>;
  get<T>(endpoint: string, params?: ApiParams, cachecode?: string | number | boolean | null): Promise<ApiResponse<T>>;
  post<T>(endpoint: string, body?: ApiBody): Promise<ApiResponse<T>>;
  put<T>(endpoint: string, body?: ApiBody): Promise<ApiResponse<T>>;
  delete<T>(endpoint: string, params?: ApiParams, cachecode?: string | number | boolean | null): Promise<ApiResponse<T>>;
  patch<T>(endpoint: string, body?: ApiBody): Promise<ApiResponse<T>>;
}
```

### 🌐 detectBaseUrl

Detecta automáticamente la URL base desde el tag `<base>` del documento con fallback inteligente.

```typescript
function detectBaseUrl(): string;
```

**Comportamiento avanzado:**

- ✅ Detecta tag `<base href="...">` en el documento
- ✅ Fallback a `window.location.origin`
- ✅ Compatible con SSR (retorna cadena vacía)
- ✅ Normalización automática (elimina barras finales)
- ✅ Validación de URLs válidas

### ⚙️ fetcher.configure

Configura los ajustes globales con soporte para autenticación.

```typescript
interface RequestConfig {
  timeout?: number;      // Timeout en ms (debe ser entero positivo)
  base?: string;         // URL base para peticiones
  headers?: Record<string, string>; // Headers HTTP por defecto
  bearerToken?: string;  // Token Bearer para Authorization header
  headerToken?: string;  // Token personalizado para header Token
}

fetcher.configure(config: Partial<RequestConfig>): void;
```

**Ejemplos de configuración:**

```typescript
// Autenticación Bearer
fetcher.configure({
  bearerToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  base: "https://api.miapp.com",
});
// Automáticamente añade: Authorization: Bearer <token>

// Headers personalizados
fetcher.configure({
  headers: {
    "X-API-Key": "mi-api-key",
    "Content-Type": "application/json",
  },
  headerToken: "custom-token-value",
});
// Añade: Token: custom-token-value
```

**Validaciones automáticas:**

- ✅ Timeout debe ser entero positivo
- ✅ Headers debe ser objeto válido
- ✅ Tokens deben ser strings

### 🆕 Métodos HTTP Directos (Nuevo en v1.0.2)

La clase Fetcher ahora incluye métodos HTTP directos para un código más limpio y expresivo:

#### fetcher.get() - Peticiones GET

```typescript
function fetcher.get<T>(endpoint: string, params?: ApiParams, cachecode?: string | number | boolean | null, timeout?: number): Promise<ApiResponse<T>>;
```

```typescript
// GET básico
const users = await fetcher.get<User[]>("/users");

// GET con parámetros
const activeUsers = await fetcher.get<User[]>("/users", { active: true, limit: 10 });

// GET con cache
const cachedData = await fetcher.get("/config", {}, true);

// GET con timeout personalizado (5 segundos)
const quickData = await fetcher.get<Data>("/quick-data", {}, false, 5000);
```

#### fetcher.post() - Peticiones POST

```typescript
function fetcher.post<T>(endpoint: string, body?: ApiBody, timeout?: number): Promise<ApiResponse<T>>;
```

```typescript
// Crear nuevo usuario
const newUser = await fetcher.post<User>("/users", {
  name: "Juan Pérez",
  email: "juan@ejemplo.com",
});

// POST con timeout personalizado (10 segundos para operaciones pesadas)
const uploadResult = await fetcher.post<Result>("/upload", largeFileData, 10000);
```

#### fetcher.put() - Peticiones PUT

```typescript
function fetcher.put<T>(endpoint: string, body?: ApiBody, timeout?: number): Promise<ApiResponse<T>>;
```

```typescript
// Actualizar usuario completo
const updatedUser = await fetcher.put<User>("/users/123", {
  name: "Juan Carlos Pérez",
  email: "juan.carlos@ejemplo.com",
});

// PUT con timeout largo para operaciones de sincronización (30 segundos)
const syncResult = await fetcher.put<SyncStatus>("/sync-data", bulkData, 30000);
```

#### fetcher.delete() - Peticiones DELETE

```typescript
function fetcher.delete<T>(endpoint: string, params?: ApiParams, cachecode?: string | number | boolean | null, timeout?: number): Promise<ApiResponse<T>>;
```

```typescript
// Eliminar usuario por ID
await fetcher.delete("/users/123");

// Eliminar con parámetros
await fetcher.delete("/users", { inactive: true });

// DELETE con timeout personalizado para limpieza de datos (20 segundos)
await fetcher.delete("/cleanup", {}, null, 20000);
```

#### fetcher.patch() - Peticiones PATCH

```typescript
function fetcher.patch<T>(endpoint: string, body?: ApiBody, timeout?: number): Promise<ApiResponse<T>>;
```

```typescript
// Actualizar parcialmente
const patched = await fetcher.patch<User>("/users/123", {
  email: "nuevo@email.com",
});

// PATCH con timeout corto para actualizaciones críticas (2 segundos)
const urgentUpdate = await fetcher.patch<Status>("/status", { active: false }, 2000);
```

#### fetcher.request() - Método Genérico

```typescript
function fetcher.request<T>(endpoint: string, method?: HttpMethod, data?: ApiParams | ApiBody, cachecode?: string | number | boolean | null, timeout?: number): Promise<ApiResponse<T>>;
```

```typescript
// Detección automática: GET usa parámetros, POST usa cuerpo
const users = await fetcher.request<User[]>("/users", "GET", { active: true });
const newUser = await fetcher.request<User>("/users", "POST", { name: "Ana" });

// Con timeout personalizado según el tipo de operación
const quickSearch = await fetcher.request<SearchResult>("/search", "GET", { q: "test" }, null, 3000);
const dataImport = await fetcher.request<ImportResult>("/import", "POST", csvData, null, 60000);
```

### 🌟 Ventajas de los Métodos Directos

- ✅ **Más expresivo**: `fetcher.get()` vs `fetchWithParams()`
- ✅ **Menos imports**: Todo en la instancia principal
- ✅ **Mejor autocompletado**: IntelliSense mejorado en IDEs
- ✅ **Compatibilidad total**: Los métodos anteriores siguen funcionando
- ✅ **Timeout granular**: Control individual del timeout por petición

### ⏱️ Control de Timeout Personalizado (v1.0.4)

Todos los métodos HTTP ahora soportan un parámetro `timeout` opcional para control granular:

```typescript
// Timeouts según el tipo de operación
const quickData = await fetcher.get("/live-data", {}, false, 2000); // 2s - datos en vivo
const searchResults = await fetcher.get("/search", { q: "query" }, false, 5000); // 5s - búsqueda
const fileUpload = await fetcher.post("/upload", fileData, 30000); // 30s - subida de archivos
const bulkOperation = await fetcher.put("/bulk-update", data, 60000); // 60s - operaciones masivas
const cleanup = await fetcher.delete("/cleanup", {}, null, 45000); // 45s - limpieza de datos
const statusUpdate = await fetcher.patch("/status", { active: true }, 1000); // 1s - actualización crítica

// Si no se especifica timeout, usa la configuración global
const defaultTimeout = await fetcher.get("/data"); // Usa fetcher timeout configurado
```

**Características del timeout:**

- ✅ **Fallback inteligente**: Usa configuración global si no se especifica
- ✅ **Validación automática**: Debe ser un número entero positivo
- ✅ **Control granular**: Cada petición puede tener su propio timeout
- ✅ **AbortController**: Cancelación nativa de peticiones
- ✅ **Sin afectar global**: No modifica la configuración general

### 🔄 Migración Fácil

```typescript
// Antes
const users = await fetchWithParams("/users", "GET", { active: true });
const newUser = await fetchWithBody("/users", "POST", { name: "Juan" });

// Ahora (más limpio)
const users = await fetcher.get("/users", { active: true });
const newUser = await fetcher.post("/users", { name: "Juan" });
```

### 📤 Upload y Download con Progreso (v1.1.0)

#### fetcher.upload() - Subir archivos

```typescript
function fetcher.upload<T>(
  endpoint: string,
  file: string | File | Blob,
  fileparam?: string,
  onprogress?: (event: ProgressEvent) => void,
  body?: ApiBody,
  timeout?: number,
  method?: string
): Promise<ApiResponse<T>>;
```

**Navegador:**

```typescript
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];

const result = await fetcher.upload(
  "/api/upload",
  file,
  "avatar",
  (e) => {
    console.log(`Progreso: ${e.percent.toFixed(2)}%`);
    console.log(`${e.loaded} / ${e.total} bytes`);
  },
  { userId: 123, category: "profile" }
);
```

**Node.js:**

```typescript
const result = await fetcher.upload(
  "/api/upload",
  "./documents/invoice.pdf", // Ruta del archivo
  "document",
  (e) => console.log(`${e.percent.toFixed(2)}%`),
  { documentType: "invoice" }
);
```

#### fetcher.download() - Descargar archivos

```typescript
function fetcher.download<T>(
  endpoint: string,
  filename?: string,
  onprogress?: (event: ProgressEvent) => void,
  params?: ApiParams,
  cachecode?: string | number | boolean | null,
  timeout?: number,
  method?: string
): Promise<ApiResponse<T>>;
```

**Navegador (descarga automática):**

```typescript
const result = await fetcher.download("/api/files/report.pdf", "informe-mensual.pdf", (e) => {
  const percent = e.percent.toFixed(2);
  console.log(`Descargando: ${percent}%`);
});
// El archivo se descarga automáticamente
```

**Node.js (guarda en disco):**

```typescript
const result = await fetcher.download("/api/files/backup.zip", "./downloads/backup.zip", (e) => console.log(`${e.loaded} / ${e.total} bytes`));
// Archivo guardado en ./downloads/backup.zip
```

#### appendObjectToForm() - Helper para FormData

```typescript
import { appendObjectToForm } from "@jysperu/helper-fetcher";

const form = new FormData();
appendObjectToForm(form, {
  name: "Juan Pérez",
  age: 30,
  avatar: fileInput.files[0], // File
  metadata: { role: "admin", active: true }, // Se serializa a JSON
  optional: undefined, // Se omite
});

await fetcher.post("/api/profile", form);
```

## 📚 API Reference Clásica

Todos los métodos anteriores siguen siendo completamente compatibles:

### 🌐 Funciones de Petición

#### api_get - GET con parámetros

```typescript
function api_get<T = unknown>(endpoint: string, params?: ApiParams, cachecode?: string | number | boolean | null): Promise<ApiResponse<T>>;
```

#### api_post - POST con cuerpo

```typescript
function api_post<T = unknown>(endpoint: string, body?: ApiBody): Promise<ApiResponse<T>>;
```

#### api_request - Método genérico

```typescript
function api_request<T = unknown>(
  endpoint: string,
  method?: HttpMethod,
  data?: ApiParams | ApiBody,
  cacheCode?: string | number | boolean | null
): Promise<ApiResponse<T>>;
```

#### api - Función principal

```typescript
function api<T = unknown>(
  endpoint: string,
  bodyOrParams?: ApiParams | ApiBody,
  cacheCode?: string | number | boolean | null,
  method?: HttpMethod
): Promise<ApiResponse<T>>;
```

### 🔧 Funciones de utilidad

#### buildUrl - Constructor de URLs

```typescript
fetcher.buildUrl(endpoint: string, params?: ApiParams, cachecode?: string | number | boolean | null): string;
```

**Maneja múltiples tipos de endpoints:**

```typescript
// URL completa - se usa directamente
fetcher.buildUrl("https://otra-api.com/datos");

// Ruta absoluta - concatena con base
fetcher.buildUrl("/api/usuarios"); // → https://mibase.com/api/usuarios

// Ruta relativa - agrega / automáticamente
fetcher.buildUrl("usuarios"); // → https://mibase.com/usuarios
```

#### fetch - Fetch con timeout

```typescript
function fetch<T = unknown>(url: string, options: RequestInit, timeout?: number): Promise<ApiResponse<T>>;
```

#### processFetchJsonResponse - Normalizador

```typescript
function processFetchJsonResponse<T = unknown>(json: any): ApiResponse<T>;
```

**Características:**

- ✅ Convierte `status: "success"` → `success: true`
- ✅ Normaliza mensajes de error y éxito
- ✅ Limpia propiedades redundantes
- ✅ Validación de JSON inválido
- ✅ Manejo robusto de tipos

## 📋 Tipos TypeScript

### ApiResponse

```typescript
interface ApiResponse<T = unknown> {
  success: boolean; // true si la operación fue exitosa
  log: string; // Mensaje de log/error
  data?: T; // Datos de respuesta (opcional)
  response?: string; // Texto original si la respuesta no es JSON (v1.0.5)
  [key: string]: unknown;
}
```

**Manejo flexible de respuestas (v1.0.5):**

```typescript
// Respuesta JSON estándar
const result = await api_get<User[]>("/users");
// { success: true, log: "Operación exitosa", data: [...] }

// Respuesta de texto plano
const textResult = await api_get("/health-check");
// { success: true, log: "OK", response: "OK" }

// Error HTTP con JSON
const error = await api_post("/invalid-endpoint", {});
// { success: false, log: "Error message from server", data: {...} }

// Error HTTP con texto plano
const htmlError = await api_get("/not-found");
// { success: false, log: "HTTP 404: Not Found", response: "<!DOCTYPE html>..." }
```

**Características del manejo de respuestas:**

- ✅ Detecta automáticamente JSON vs texto plano
- ✅ Preserva contenido original en `response` si no es JSON
- ✅ `success` siempre refleja `response.ok` (códigos HTTP 2xx)
- ✅ Maneja errores con y sin cuerpo JSON
- ✅ Compatible con APIs que retornan HTML o texto

### ApiParams

```typescript
interface ApiParams {
  [key: string]: string | number | boolean;
}
```

### ApiBody

```typescript
interface ApiBody {
  [key: string]: unknown;
}
```

### HttpMethod

```typescript
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
```

## 🌐 Detección Automática de Base URL

La librería detecta automáticamente la URL base de tu aplicación:

### Tag Base HTML

```html
<!DOCTYPE html>
<html>
  <head>
    <!-- La librería detecta automáticamente este tag -->
    <base href="https://api.miapp.com/v2/" />
  </head>
  <body>
    <!-- Tu aplicación -->
  </body>
</html>
```

```typescript
import { detectBaseUrl, api_get } from "@jysperu/helper-fetcher";

// Detecta automáticamente: "https://api.miapp.com/v2"
console.log(detectBaseUrl());

// Todas las peticiones usan esta base automáticamente
const datos = await api_get("/usuarios"); // → https://api.miapp.com/v2/usuarios
```

### Sin Tag Base

Si no hay tag `<base>`, usa `window.location.origin`:

```typescript
// En https://miapp.com/dashboard
console.log(detectBaseUrl()); // "https://miapp.com"

const datos = await api_get("/api/datos"); // → https://miapp.com/api/datos
```

## 🌐 Compatibilidad

- ✅ Todos los navegadores modernos (con soporte para fetch)
- ✅ Compatible con frameworks (React, Vue, Angular, Svelte)
- ✅ SSR friendly
- ✅ TypeScript nativo
- ✅ ES Modules y CommonJS

## 🔗 Enlaces y Recursos

- 📘 **Repositorio**: [GitLab - helper-fetcher](https://gitlab.com/tiny.node/helper/fetcher)
- 🐛 **Issues**: [Reportar problemas](https://gitlab.com/tiny.node/helper/fetcher/issues)
- 📦 **npm**: [@jysperu/helper-fetcher](https://www.npmjs.com/package/@jysperu/helper-fetcher)
- 🏢 **JYS Perú**: [www.jys.pe](https://www.jys.pe)

## 📄 Licencia

**MIT License** - Consulta el archivo [LICENSE](./LICENSE) para detalles completos.

```text
MIT License - Copyright (c) 2025 JYS Perú
```

---

Desarrollado con ❤️ por [**JYS Perú**](https://www.jys.pe)