@regcheq/http-client
v2.2.1
Published
Capa HTTP centralizada para microservicios y frontends Regcheq
Readme
@regcheq/http-client
Librería interna de cliente HTTP para todos los proyectos de Regcheq (Node.js y browser). Reemplaza el uso directo de axios con un middleware centralizado: retries, timeouts, logging, correlation ID y fix de latencia en Node 22 — todo en un solo lugar, sin repetirlo en cada servicio.
Objetivos
- Un solo
importen cada servicio; sin boilerplate de configuración. - Fix automático de latencia en Node 22 (keep-alive + orden DNS
ipv4first). - Comportamiento consistente en todos los servicios: mismos defaults, mismo manejo de errores.
- Si hay que cambiar algo global (timeout, headers, retry strategy), se cambia en esta librería — no en 16 repos.
Instalación
npm i @regcheq/http-clientUso básico
El export principal es client — un singleton que gestiona automáticamente un pool de conexiones por origin. Úsalo igual que axios:
import { client } from '@regcheq/http-client';
// GET
const data = await client.get<Empresa>(`${process.env.API_MAIN}/empresa/123`);
// POST
const created = await client.post<Empresa>(`${process.env.API_MAIN}/empresa`, { nombre: 'Regcheq' });
// PUT
const updated = await client.put<Empresa>(`${process.env.API_MAIN}/empresa/123`, { nombre: 'Regcheq SA' });
// PATCH
const patched = await client.patch<Empresa>(`${process.env.API_MAIN}/empresa/123`, { nombre: 'Regcheq SA' });
// DELETE
await client.delete(`${process.env.API_MAIN}/empresa/123`);No hay que configurar nada. La librería crea y reutiliza automáticamente los pools de conexión por origin.
Requisitos para desarrollo
El proyecto usa Node 22. Con nvm instalado:
nvm use
npm ciHusky ejecuta antes de cada commit:
- pre-commit:
typecheck,lintytest— no se permite commit si alguno falla. - commit-msg: commitlint para que los mensajes sigan Conventional Commits (
feat:,fix:,chore:, etc.).
Guía rápida por framework
NestJS
Registra HttpClientModule globalmente y usa HttpClientService:
// app.module.ts
import { HttpClientModule } from '@regcheq/http-client';
@Module({
imports: [HttpClientModule],
})
export class AppModule {}// cualquier-service.service.ts
import { HttpClientService } from '@regcheq/http-client';
@Injectable()
export class ReportesService {
constructor(private readonly http: HttpClientService) {}
async obtenerReporte(id: string) {
return this.http.get<Reporte>(`${process.env.API_INFORMES}/reportes/${id}`);
}
async crearReporte(data: CreateReporteDto) {
return this.http.post<Reporte>(`${process.env.API_INFORMES}/reportes`, data);
}
}Cliente dedicado por servicio (para timeout o retries distintos):
import { createClient, HttpClientError } from '@regcheq/http-client';
import type { OnModuleDestroy } from '@nestjs/common';
@Injectable()
export class CargaMasivaService implements OnModuleDestroy {
private api = createClient(process.env.API_FILES!);
onModuleDestroy() {
return this.api.destroy();
}
async subirArchivo(data: Buffer): Promise<UploadResult> {
return this.api.post<UploadResult>('/archivo/carga-masiva/errors', data, {
headers: { 'Content-Type': 'application/octet-stream' },
});
}
async obtenerArchivo(id: string): Promise<Buffer> {
return this.api.buffer(`/archivo/${id}`);
}
}Con timeout personalizado por llamada:
// Para uploads grandes donde el timeout global de 10s no alcanza
async subirExcel(file: Buffer) {
return this.api.post<UploadResult>('/upload/excel', file, {
headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
timeout: 60_000,
});
}Con retries activados para un endpoint específico:
const apiConRetry = createClient(process.env.API_LISTAS!, { retries: 2 });LoopBack
No hay módulo especial. Usar client directamente o createClient para opciones custom:
import { client, createClient, HttpClientError } from '@regcheq/http-client';
export class MiddlewareListasService {
// Para servicios externos más lentos, cliente dedicado con timeout propio
private api = createClient(process.env.MIDDLEWARE_LISTAS_URL!, {
timeout: 15_000,
});
async consultarLista(rut: string): Promise<ListaResult> {
return this.api.post<ListaResult>('/consulta', { rut });
}
async consultarConTimeout(rut: string): Promise<ListaResult> {
return this.api.post<ListaResult>('/consulta-batch', { rut }, {
timeout: 30_000, // sobreescribe solo esta llamada
});
}
}Múltiples destinos — usar client con URL completa:
export class GatewayService {
async procesarSolicitud(id: string) {
const [empresa, archivos] = await Promise.all([
client.get<Empresa>(`${process.env.API_MAIN}/empresa/${id}`),
client.get<Archivo[]>(`${process.env.API_FILES}/archivos?empresaId=${id}`),
]);
return { empresa, archivos };
}
}Express
import express from 'express';
import { client, HttpClientError } from '@regcheq/http-client';
const app = express();
app.get('/empresa/:id', async (req, res, next) => {
try {
const empresa = await client.get<Empresa>(`${process.env.API_MAIN}/empresa/${req.params.id}`);
res.json(empresa);
} catch (err) {
next(err);
}
});
app.post('/webhook/forward', async (req, res, next) => {
try {
const result = await client.post<unknown>(req.body.callbackUrl, req.body.payload, {
timeout: 5_000,
});
res.json(result);
} catch (err) {
next(err);
}
});Vue 2
// src/plugins/http.ts
import Vue from 'vue';
import { client } from '@regcheq/http-client';
Vue.prototype.$http = client;
// src/main.ts
import './plugins/http';// En un componente
export default Vue.extend({
data() {
return { empresa: null as Empresa | null, error: null as string | null };
},
async created() {
try {
this.empresa = await this.$http.get<Empresa>(`${process.env.VUE_APP_API_URL}/empresa/${this.id}`);
} catch (err) {
if (err instanceof HttpClientError) {
this.error = `Error ${err.statusCode}`;
}
}
},
});En Vuex:
import { client } from '@regcheq/http-client';
export const actions = {
async fetchEmpresa({ commit }, id: string) {
const empresa = await client.get<Empresa>(`${process.env.VUE_APP_API_URL}/empresa/${id}`);
commit('SET_EMPRESA', empresa);
},
};React
// src/hooks/useEmpresa.ts
import { useState, useEffect } from 'react';
import { client, HttpClientError } from '@regcheq/http-client';
export function useEmpresa(id: string) {
const [empresa, setEmpresa] = useState<Empresa | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
client.get<Empresa>(`${import.meta.env.VITE_API_URL}/empresa/${id}`)
.then(setEmpresa)
.catch((err) => {
if (err instanceof HttpClientError) {
setError(`Error ${err.statusCode}`);
}
})
.finally(() => setLoading(false));
}, [id]);
return { empresa, error, loading };
}Con React Query:
import { useQuery, useMutation } from '@tanstack/react-query';
import { client } from '@regcheq/http-client';
export function useEmpresaQuery(id: string) {
return useQuery({
queryKey: ['empresa', id],
queryFn: () => client.get<Empresa>(`${import.meta.env.VITE_API_URL}/empresa/${id}`),
});
}
export function useCrearEmpresa() {
return useMutation({
mutationFn: (data: CreateEmpresaDto) =>
client.post<Empresa>(`${import.meta.env.VITE_API_URL}/empresa`, data),
});
}Manejo de errores
Todos los errores (HTTP 4xx/5xx y de red) se normalizan en HttpClientError:
import { HttpClientError } from '@regcheq/http-client';
try {
const data = await client.get<Empresa>(`${process.env.API_MAIN}/empresa/123`);
} catch (err) {
if (err instanceof HttpClientError) {
console.log(err.statusCode); // 404, 500, 0 (error de red)
console.log(err.body); // body parseado si era JSON, string si no
console.log(err.isNetworkError); // true si no hubo respuesta del servidor
console.log(err.message); // 'HTTP 404'
}
}Diferencias con axios:
// ANTES (axios)
} catch (err) {
if (axios.isAxiosError(err)) {
console.log(err.response?.status);
console.log(err.response?.data);
}
}
// DESPUÉS (@regcheq/http-client)
} catch (err) {
if (err instanceof HttpClientError) {
console.log(err.statusCode);
console.log(err.body);
}
}Configuración de timeouts
Por variable de entorno (aplica a todos los clientes):
HTTP_TIMEOUT_MS=10000Al crear un cliente dedicado (sobreescribe la variable de entorno):
const api = createClient(process.env.API_LENTA!, { timeout: 30_000 });Por llamada individual:
await client.post('/upload', file, { timeout: 60_000 });Prioridad: llamada > cliente dedicado > variable de entorno > default (10 segundos).
Configuración de retries
Por defecto no hay retries. Los retries solo aplican a errores de red (isNetworkError = true), nunca a errores HTTP (4xx/5xx).
// Sin retries (default)
const api = createClient(process.env.API_URL!);
// Con 2 retries para un servicio con intermitencia
const apiConRetry = createClient(process.env.API_LISTAS!, { retries: 2 });El delay entre reintentos usa backoff exponencial: 200ms, 400ms, 800ms...
Por variable de entorno:
HTTP_RETRIES=2
HTTP_RETRY_DELAY_MS=200Correlation ID (propagación automática)
En servicios que reciben un x-request-id y hacen llamadas a otros servicios, usar runWithCorrelationId para propagarlo automáticamente:
import { runWithCorrelationId } from '@regcheq/http-client';
// En el middleware de tu servicio (NestJS/Express/LoopBack)
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] as string ?? crypto.randomUUID();
runWithCorrelationId(requestId, () => next());
});Todas las llamadas HTTP dentro de ese contexto incluirán el x-request-id automáticamente.
Migración desde axios
Llamada directa:
// ANTES
import axios from 'axios';
const response = await axios.get(`${process.env.API_FILES}/archivo/${id}`);
const data = response.data; // axios envuelve en { data }
// DESPUÉS
import { client } from '@regcheq/http-client';
const data = await client.get<Archivo>(`${process.env.API_FILES}/archivo/${id}`);
// retorna el body directamente — quitar .dataaxios.create() → createClient():
// ANTES
const api = axios.create({ baseURL: process.env.API_FILES, timeout: 8000 });
const response = await api.post('/archivo', payload);
const data = response.data;
// DESPUÉS
import { createClient } from '@regcheq/http-client';
const api = createClient(process.env.API_FILES!, { timeout: 8000 });
const data = await api.post<UploadResult>('/archivo', payload);Diferencias clave:
| Comportamiento | axios | @regcheq/http-client |
|---|---|---|
| Respuesta exitosa | response.data | valor directo |
| Error HTTP | err.response.status + err.response.data | err.statusCode + err.body |
| Body en POST/PUT/PATCH | segundo argumento | segundo argumento |
| Content-Type | manual | automático si body es objeto |
| Timeout | { timeout: ms } | { timeout: ms } |
| Cancelación | CancelToken (deprecated) | { signal: AbortSignal } |
Variables de entorno disponibles
Todos tienen valores por defecto — solo definir si se necesita sobreescribir:
HTTP_TIMEOUT_MS=10000 # Timeout global en ms (default: 10000)
HTTP_MAX_CONNECTIONS=10 # Conexiones por pool (default: 10, solo Node.js)
HTTP_RETRIES=0 # Reintentos ante error de red (default: 0)
HTTP_RETRY_DELAY_MS=200 # Delay base del backoff exponencial (default: 200)
HTTP_LOG_REQUESTS=false # Log de cada request/response (default: false)
HTTP_CORRELATION_HEADER=x-request-id # Header de correlación (default: x-request-id)Notas de rendimiento en Node 22
Esta librería aplica automáticamente los fixes necesarios para Node 22:
- Keep-alive: las conexiones TCP se reutilizan (undici Pool). Con axios sin esta librería cada request abría y cerraba una conexión, causando +100-500ms de overhead por request en K8s.
- DNS
ipv4first: Node 22 cambió el orden DNS averbatim, causando que en Kubernetes los lookups IPv6 fallaran primero y después hicieran fallback a IPv4 (+50-200ms en DNS frío). Esta librería revierte al comportamiento de Node 18.
Estos dos fixes se aplican globalmente al importar la librería — no requieren configuración adicional.
Versioning (SemVer)
Este paquete sigue SemVer:
PATCH: fixes internos sin cambios de API.MINOR: nuevas funcionalidades compatibles.MAJOR: cambios que rompen compatibilidad.
Commits (Conventional Commits)
El release automático usa Conventional Commits. Ejemplos:
feat: agregar soporte para FormData→ MINORfix: corregir timeout en pool de conexiones→ PATCHfeat!: cambiar API de createClient→ MAJOR
Tipos comunes: feat, fix, chore, docs, refactor, test.
