@desarrollo-reino/errors
v1.0.3
Published
Paquete compartido para manejo consistente de errores en aplicaciones NestJS y React/NextJS
Readme
@reino/errors
Paquete compartido para manejo consistente de errores en aplicaciones NestJS y React/NextJS con arquitectura modular y separación de responsabilidades.
🎯 Ventajas
- ✅ Consistencia: Mismo manejo de errores en backend y frontend
- ✅ Escalabilidad: Fácil de extender con nuevos tipos de error
- ✅ Multiidioma: Soporte para español e inglés (extensible)
- ✅ TypeScript: Tipado completo para mejor DX
- ✅ Principios SOLID: Código mantenible y extensible
- ✅ Fácil mantenimiento: Diseñado para que juniors puedan trabajar sin problemas
- ✅ Arquitectura modular: Separación clara de responsabilidades
- ✅ Type Safety: Sin strings hardcodeados, solo constantes tipadas
📦 Instalación
npm install @reino/errors
# o
yarn add @reino/errors🏗️ Nueva Estructura Modular
@reino/errors/
├── src/
│ ├── types/ # 📁 Tipos TypeScript
│ │ ├── index.ts # Exportación principal de tipos
│ │ ├── error-codes.types.ts # Tipos para códigos de error
│ │ ├── error-details.types.ts # Tipos para detalles de error
│ │ └── messages.types.ts # Tipos para mensajes y localización
│ │
│ ├── constants/ # 📁 Constantes y configuraciones
│ │ ├── index.ts # Exportación principal de constantes
│ │ ├── error-codes.constants.ts # Códigos de error organizados
│ │ └── messages.constants.ts # Mensajes localizados
│ │
│ ├── utils/ # 📁 Utilidades y funciones puras
│ │ ├── index.ts # Exportación principal de utils
│ │ └── message-utils.ts # Utilidades para mensajes
│ │
│ ├── errors/ # 📁 Clases de error especializadas
│ │ ├── index.ts # Exportación principal de errores
│ │ ├── base/
│ │ │ └── app-error.ts # Clase base AppError
│ │ ├── auth/
│ │ │ └── auth-error.ts # Errores de autenticación
│ │ ├── validation/
│ │ │ └── validation-error.ts # Errores de validación
│ │ ├── business/
│ │ │ └── business-error.ts # Errores de negocio
│ │ └── server/
│ │ └── server-error.ts # Errores del servidor
│ │
│ ├── factories/ # 📁 Factories para creación de errores
│ │ ├── index.ts # Exportación principal de factories
│ │ └── error-factory.ts # Factory principal de errores
│ │
│ └── index.ts # 🚀 Punto de entrada principal
│
├── examples/
│ └── error-examples.ts # Ejemplos de uso prácticos
├── package.json
├── tsconfig.json
└── README.md🔧 Uso Básico Actualizado
Importar constantes y clases (Recomendado)
import {
// Clases de error
AppError,
ValidationError,
AuthError,
BusinessError,
ServerError,
// Factory
ErrorFactory,
// Constantes organizadas por categoría
AUTH_ERROR_CODES,
VALIDATION_ERROR_CODES,
BUSINESS_ERROR_CODES,
SERVER_ERROR_CODES,
// Constante general (retrocompatibilidad)
ERROR_CODES,
// Utilidades
getErrorMessage,
} from "@reino/errors";✅ Crear errores usando constantes (Type Safe)
// ✅ RECOMENDADO: Usando constantes tipadas
const authError = new AuthError(AUTH_ERROR_CODES.UNAUTHORIZED);
const validationError = new ValidationError(
VALIDATION_ERROR_CODES.REQUIRED_FIELD,
{ field: "email" }
);
const businessError = new BusinessError(
BUSINESS_ERROR_CODES.PRODUCT_NOT_FOUND,
404,
{ resourceId: "123" }
);
// ❌ EVITAR: Strings hardcodeados
const badError = new AppError("AUTH_UNAUTHORIZED", 401); // No type safe!Usar métodos estáticos (aún más fácil)
// Validación
const emailRequired = ValidationError.fieldRequired("email");
const invalidFormat = ValidationError.invalidFormat("phone", "+123");
const weakPassword = ValidationError.weakPassword();
// Autenticación
const unauthorized = AuthError.unauthorized();
const tokenExpired = AuthError.tokenExpired();
const forbidden = AuthError.forbidden("delete_user");
// Negocio
const productNotFound = BusinessError.productNotFound("PROD-123");
const userNotFound = BusinessError.userNotFound("USER-456");
const stockError = BusinessError.insufficientStock("PROD-123", 5, 2);
// Servidor
const internalError = ServerError.internal();
const dbError = ServerError.database("user_query", "users", "SELECT");
const networkError = ServerError.network("https://api.example.com");ErrorFactory con nuevas funcionalidades
// Crear automáticamente el tipo correcto
const error = ErrorFactory.create(BUSINESS_ERROR_CODES.PRODUCT_NOT_FOUND, {
resourceId: "PROD-123",
});
// Crear desde respuesta de API
const apiError = ErrorFactory.fromApiResponse({
error: {
code: "BUSINESS_PRODUCT_NOT_FOUND",
message: "Product not found",
statusCode: 404,
},
});
// Crear múltiples errores de validación
const fieldErrors = ErrorFactory.createValidationErrors({
email: VALIDATION_ERROR_CODES.INVALID_EMAIL,
password: VALIDATION_ERROR_CODES.PASSWORD_TOO_WEAK,
});
// Agrupar errores por categoría
const groupedErrors = ErrorFactory.groupErrorsByCategory([
authError,
validationError,
businessError,
]);
console.log(groupedErrors);
// {
// auth: [authError],
// validation: [validationError],
// business: [businessError]
// }🎯 Uso en NestJS (Backend) - Actualizado
Exception Filter Mejorado
// common/filters/app-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
Logger,
} from "@nestjs/common";
import { Request, Response } from "express";
import { AppError, ErrorFactory } from "@reino/errors";
@Catch()
export class AppExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(AppExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let appError: AppError;
if (exception instanceof AppError) {
appError = exception;
} else if (exception instanceof HttpException) {
appError = ErrorFactory.fromUnknown(exception);
} else {
appError = ErrorFactory.fromUnknown(exception);
}
// Logging estructurado
this.logger.error("Error occurred", {
error: appError.toJSON(),
url: request.url,
method: request.method,
ip: request.ip,
userAgent: request.get("User-Agent"),
});
// Respuesta consistente usando factory
const errorResponse = ErrorFactory.toApiResponse(appError);
response.status(appError.statusCode).json({
...errorResponse.error,
path: request.url,
method: request.method,
timestamp: new Date().toISOString(),
});
}
}En un Service con constantes
// products/products.service.ts
import { Injectable, Logger } from "@nestjs/common";
import {
BusinessError,
ValidationError,
ServerError,
BUSINESS_ERROR_CODES,
VALIDATION_ERROR_CODES,
} from "@reino/errors";
@Injectable()
export class ProductsService {
private readonly logger = new Logger(ProductsService.name);
async findById(id: string) {
if (!id) {
// ✅ Usando constante tipada
throw ValidationError.fieldRequired("id");
}
if (id.length < 3) {
// ✅ Usando constante tipada
throw new ValidationError(VALIDATION_ERROR_CODES.FIELD_TOO_SHORT, {
field: "id",
minLength: 3,
currentLength: id.length,
});
}
try {
const product = await this.productRepository.findById(id);
if (!product) {
// ✅ Usando método estático
throw BusinessError.productNotFound(id);
}
return product;
} catch (error) {
if (error instanceof AppError) {
throw error; // Re-lanzar errores de aplicación
}
// Convertir errores de BD a errores consistentes
this.logger.error("Database error in findById", { error, productId: id });
throw ServerError.database("findById", "products", "SELECT");
}
}
async updateStock(productId: string, quantity: number) {
const product = await this.findById(productId);
if (product.stock < quantity) {
// ✅ Usando método estático con detalles específicos
throw BusinessError.insufficientStock(productId, quantity, product.stock);
}
// Lógica de actualización...
return this.productRepository.updateStock(productId, quantity);
}
}⚛️ Uso en React/NextJS (Frontend) - Actualizado
Hook mejorado para manejo de errores
// hooks/useErrorHandler.ts
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import {
AppError,
ErrorFactory,
BusinessError,
AuthError,
ValidationError,
// ✅ Importar constantes para comparaciones type-safe
AUTH_ERROR_CODES,
BUSINESS_ERROR_CODES,
VALIDATION_ERROR_CODES,
SERVER_ERROR_CODES,
} from "@reino/errors";
import { toast } from "react-hot-toast";
export const useErrorHandler = () => {
const navigate = useNavigate();
const handleError = useCallback(
(error: unknown) => {
const appError = ErrorFactory.isAppError(error)
? error
: ErrorFactory.fromUnknown(error);
// ✅ Manejo específico usando constantes tipadas
switch (appError.code) {
case AUTH_ERROR_CODES.UNAUTHORIZED:
case AUTH_ERROR_CODES.TOKEN_EXPIRED:
toast.error("Sesión expirada. Redirigiendo...");
navigate("/login");
break;
case AUTH_ERROR_CODES.FORBIDDEN:
toast.error("No tienes permisos para esta acción");
break;
case BUSINESS_ERROR_CODES.PRODUCT_NOT_FOUND:
toast.error("Producto no encontrado");
if (location.pathname.includes("/products/")) {
navigate("/products");
}
break;
case BUSINESS_ERROR_CODES.INSUFFICIENT_STOCK:
const stockError = appError as BusinessError;
toast.error(
`Stock insuficiente. Disponible: ${stockError.details?.availableQuantity}`
);
break;
case VALIDATION_ERROR_CODES.REQUIRED_FIELD:
toast.error(
`Campo requerido: ${(appError as ValidationError).getFieldName()}`
);
break;
case SERVER_ERROR_CODES.NETWORK_ERROR:
toast.error("Error de conexión. Verifica tu internet.");
break;
default:
toast.error(appError.message);
}
// Log para debugging (solo en desarrollo)
if (process.env.NODE_ENV === "development") {
console.group("🐛 Error Details");
console.error("Error:", appError.toJSON());
console.groupEnd();
}
return appError;
},
[navigate]
);
return { handleError };
};Componente con manejo type-safe
// components/ProductDetail.tsx
import React, { useEffect, useState } from "react";
import { useErrorHandler } from "../hooks/useErrorHandler";
import {
BusinessError,
BUSINESS_ERROR_CODES, // ✅ Importar constantes
SERVER_ERROR_CODES,
} from "@reino/errors";
import api from "../services/api";
interface Product {
id: string;
name: string;
stock: number;
price: number;
}
export const ProductDetail: React.FC<{ productId: string }> = ({
productId,
}) => {
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { handleError } = useErrorHandler();
useEffect(() => {
const fetchProduct = async () => {
try {
setLoading(true);
setError(null);
const response = await api.get(`/products/${productId}`);
setProduct(response.data);
} catch (error) {
const appError = handleError(error);
// ✅ Manejo específico usando constantes (Type Safe!)
if (appError instanceof BusinessError) {
switch (appError.code) {
case BUSINESS_ERROR_CODES.PRODUCT_NOT_FOUND:
setError("Producto no encontrado");
setProduct(null);
break;
case BUSINESS_ERROR_CODES.INSUFFICIENT_STOCK:
setError("Producto sin stock");
break;
default:
setError("Error de negocio desconocido");
}
} else if (appError.code === SERVER_ERROR_CODES.NETWORK_ERROR) {
setError("Error de conexión. Intenta nuevamente.");
} else {
setError("Ha ocurrido un error inesperado");
}
} finally {
setLoading(false);
}
};
if (productId) {
fetchProduct();
}
}, [productId, handleError]);
if (loading) return <div className="loading">Cargando producto...</div>;
if (error) return <div className="error">{error}</div>;
if (!product) return <div className="not-found">Producto no encontrado</div>;
return (
<div className="product-detail">
<h1>{product.name}</h1>
<p>Precio: ${product.price}</p>
<p>Stock: {product.stock}</p>
</div>
);
};API Client mejorado
// services/api.ts
import axios from "axios";
import {
ErrorFactory,
SERVER_ERROR_CODES,
AUTH_ERROR_CODES,
} from "@reino/errors";
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
});
// Request interceptor
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem("auth_token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(ErrorFactory.fromUnknown(error))
);
// Response interceptor mejorado
api.interceptors.response.use(
(response) => response,
(error) => {
// Si el backend devuelve errores en formato estándar
if (error.response?.data?.error) {
const appError = ErrorFactory.fromApiResponse(error.response.data);
// Manejo automático de tokens expirados
if (appError.code === AUTH_ERROR_CODES.TOKEN_EXPIRED) {
localStorage.removeItem("auth_token");
window.location.href = "/login";
}
return Promise.reject(appError);
}
// Error de red o timeout
if (error.code === "NETWORK_ERROR" || error.code === "ECONNABORTED") {
const networkError = ErrorFactory.create(
SERVER_ERROR_CODES.NETWORK_ERROR
);
return Promise.reject(networkError);
}
// Cualquier otro error
return Promise.reject(ErrorFactory.fromUnknown(error));
}
);
export default api;🌍 Soporte Multiidioma Mejorado
import {
getErrorMessage,
normalizeLanguage,
hasErrorMessage,
getAllMessages,
getLocalizationInfo,
BUSINESS_ERROR_CODES,
} from "@reino/errors";
// Funciones avanzadas de localización
const userLanguage = "en-US";
const normalizedLang = normalizeLanguage(userLanguage); // 'en'
// Verificar si existe mensaje
if (hasErrorMessage(BUSINESS_ERROR_CODES.PRODUCT_NOT_FOUND, "fr")) {
// Solo mostrar si existe traducción
}
// Obtener todos los mensajes para un idioma
const allSpanishMessages = getAllMessages("es");
// Información sobre idiomas soportados
const localizationInfo = getLocalizationInfo();
console.log(localizationInfo);
// {
// defaultLanguage: 'es',
// fallbackLanguage: 'es',
// supportedLanguages: ['es', 'en'],
// availableLanguages: ['es', 'en']
// }
// Error con idioma específico
const englishError = new BusinessError(
BUSINESS_ERROR_CODES.PRODUCT_NOT_FOUND,
404,
{ resourceId: "PROD-123" },
"en"
);
// Cambiar idioma dinámicamente
const spanishVersion = englishError.withLanguage("es");🔧 Extensión del Paquete (Actualizada)
1. Agregar nuevos tipos
// src/types/error-codes.types.ts
export type PaymentErrorCode =
| "PAYMENT_FAILED"
| "PAYMENT_DECLINED"
| "PAYMENT_TIMEOUT";
// Actualizar el tipo principal
export type ErrorCode =
| AuthErrorCode
| ValidationErrorCode
| BusinessErrorCode
| ServerErrorCode
| PaymentErrorCode; // ✅ Agregar nueva categoría2. Agregar constantes
// src/constants/error-codes.constants.ts
export const PAYMENT_ERROR_CODES: Record<string, PaymentErrorCode> = {
FAILED: "PAYMENT_FAILED",
DECLINED: "PAYMENT_DECLINED",
TIMEOUT: "PAYMENT_TIMEOUT",
} as const;
// Actualizar ERROR_CODES principal
export const ERROR_CODES = {
...AUTH_ERROR_CODES,
...VALIDATION_ERROR_CODES,
...BUSINESS_ERROR_CODES,
...SERVER_ERROR_CODES,
...PAYMENT_ERROR_CODES, // ✅ Incluir nuevos códigos
} as const;
// Actualizar mapeo de categorías
export const ERROR_CODE_CATEGORIES = {
// ... códigos existentes
[PAYMENT_ERROR_CODES.FAILED]: "payment",
[PAYMENT_ERROR_CODES.DECLINED]: "payment",
[PAYMENT_ERROR_CODES.TIMEOUT]: "payment",
} as const;3. Agregar mensajes
// src/constants/messages.constants.ts
export const SPANISH_MESSAGES: ErrorMessageMap = {
// ... mensajes existentes
PAYMENT_FAILED: "El pago no pudo ser procesado.",
PAYMENT_DECLINED: "El pago fue rechazado.",
PAYMENT_TIMEOUT: "El pago expiró por tiempo de espera.",
};
export const ENGLISH_MESSAGES: ErrorMessageMap = {
// ... mensajes existentes
PAYMENT_FAILED: "Payment could not be processed.",
PAYMENT_DECLINED: "Payment was declined.",
PAYMENT_TIMEOUT: "Payment timed out.",
};4. Crear nueva clase especializada
// src/errors/payment/payment-error.ts
import { AppError } from "../base/app-error";
import type { PaymentErrorCode } from "../../types/error-codes.types";
import type { ErrorDetails } from "../../types/error-details.types";
import type { SupportedLanguage } from "../../types/messages.types";
import { PAYMENT_ERROR_CODES } from "../../constants/error-codes.constants";
export class PaymentError extends AppError {
constructor(
code: PaymentErrorCode,
details?: ErrorDetails,
language: SupportedLanguage = "es"
) {
super(code, 402, details, language);
this.name = "PaymentError";
}
static failed(
paymentMethod: string,
amount: number,
reason?: string,
language: SupportedLanguage = "es"
): PaymentError {
return new PaymentError(
PAYMENT_ERROR_CODES.FAILED,
{
context: "Payment processing failed",
paymentMethod,
amount,
reason,
},
language
);
}
static declined(
cardLast4: string,
language: SupportedLanguage = "es"
): PaymentError {
return new PaymentError(
PAYMENT_ERROR_CODES.DECLINED,
{
context: "Payment declined by bank",
cardLast4,
},
language
);
}
}5. Actualizar el factory
// src/factories/error-factory.ts
import { PaymentError } from "../errors/payment/payment-error";
export class ErrorFactory {
static create(
code: ErrorCode,
details?: ErrorDetails,
language: SupportedLanguage = "es"
): AppError {
const category = ERROR_CODE_CATEGORIES[code];
switch (category) {
case "auth":
return new AuthError(code as any, details as any, language);
case "validation":
return new ValidationError(code as any, details as any, language);
case "business":
return new BusinessError(code as any, 422, details as any, language);
case "server":
return new ServerError(code as any, details as any, language);
case "payment": // ✅ Nueva categoría
return new PaymentError(code as any, details, language);
default:
return new AppError(code, 400, details, language);
}
}
}📝 Ejemplos de Respuesta (Actualizados)
Respuesta exitosa con nueva estructura
{
"success": false,
"error": {
"code": "BUSINESS_PRODUCT_NOT_FOUND",
"message": "El producto solicitado no fue encontrado.",
"statusCode": 404,
"details": {
"context": "product not found",
"resourceType": "product",
"resourceId": "PROD-123"
},
"timestamp": "2024-01-15T10:30:00.000Z",
"path": "/products/PROD-123",
"method": "GET"
}
}Error de validación múltiple
{
"success": false,
"error": {
"code": "VALIDATION_REQUIRED_FIELD",
"message": "Este campo es obligatorio.",
"statusCode": 400,
"details": {
"field": "email",
"context": "Field validation failed"
},
"timestamp": "2024-01-15T10:30:00.000Z",
"additionalErrors": [
{
"code": "VALIDATION_PASSWORD_TOO_WEAK",
"message": "La contraseña debe tener al menos 8 caracteres...",
"statusCode": 400,
"details": {
"field": "password",
"expectedFormat": "At least 8 characters..."
}
}
]
}
}🚀 Comandos Útiles
# Desarrollo
npm run build # Compilar TypeScript
npm run dev # Compilar en modo watch
# Publicación
npm run build # Asegurar build limpio
npm publish --access restricted
# En tus proyectos
npm install @reino/errors@latest
npm update @reino/errors🎯 Mejores Prácticas
✅ Hacer
// ✅ Usar constantes tipadas
if (error.code === BUSINESS_ERROR_CODES.PRODUCT_NOT_FOUND) {
}
// ✅ Usar métodos estáticos cuando sea posible
throw BusinessError.productNotFound(productId);
// ✅ Usar ErrorFactory para casos genéricos
const error = ErrorFactory.create(code, details);
// ✅ Agrupar imports por categoría
import {
BUSINESS_ERROR_CODES,
BusinessError,
ErrorFactory,
} from "@reino/errors";❌ Evitar
// ❌ Strings hardcodeados
if (error.code === "BUSINESS_PRODUCT_NOT_FOUND") {
}
// ❌ Crear errores manualmente sin Factory
const error = new AppError("SOME_CODE", 400);
// ❌ No manejar categorías específicas
// Siempre usar el tipo más específico disponible📋 Principios SOLID Aplicados
S (Single Responsibility): Cada archivo tiene una responsabilidad específica
/types- Solo definiciones de tipos/constants- Solo constantes y configuraciones/utils- Solo funciones puras/errors- Solo clases de error/factories- Solo lógica de creación
O (Open/Closed): Fácil de extender sin modificar código existente
- Agregar nuevas categorías sin tocar las existentes
- Nuevas clases de error heredan de AppError
L (Liskov Substitution): Todas las clases de error son intercambiables
- Cualquier clase de error puede usarse como AppError
I (Interface Segregation): Interfaces específicas y no sobrecargadas
- Tipos separados por responsabilidad
- Detalles específicos por tipo de error
D (Dependency Inversion): Depende de abstracciones, no implementaciones
- ErrorFactory abstrae la creación de errores
- Interfaces tipadas en lugar de implementaciones concretas
¡La estructura modular está lista! 🎉
Este paquete ahora proporciona una arquitectura robusta, type-safe y escalable para manejo de errores con separación clara de responsabilidades y principios SOLID.
