Mejores Prácticas
Guías listas para producción para integrar con la API de Partner de LosCenotes.
Mejores Prácticas de Seguridad
1. Gestión de Claves API
Almacenar Claves de Forma Segura
// ✅ Bueno - Variables de entorno
const apiKey = process.env.LOSCENOTES_API_KEY;
// ✅ Bueno - Servicio de gestión de claves seguro
const apiKey = await keyVault.getSecret("loscenotes-api-key");
// ❌ Malo - Claves codificadas directamente
const apiKey = "sk_test_1234567890abcdef";
// ❌ Malo - Exposición del lado del cliente
const client = new LosCenotesClient({
apiKey: "sk_test_1234567890abcdef", // ¡Nunca en código frontend!
});
Rotar Claves Regularmente
class ApiKeyRotator {
async rotateKey(): Promise<void> {
// Generar nueva clave vía API del Portal de Partner
const newKey = await this.generateNewKey();
// Actualizar todas las instancias
await this.updateDeployments(newKey);
// Verificar que la nueva clave funciona
await this.verifyNewKey(newKey);
// Revocar clave antigua
await this.revokeOldKey();
console.log("Rotación de clave API completada exitosamente");
}
private async verifyNewKey(apiKey: string): Promise<void> {
const client = new LosCenotesClient({ apiKey });
const response = await client.auth.verify();
if (!response.success) {
throw new Error("Verificación de nueva clave API falló");
}
}
}
2. Seguridad de Solicitudes
Usar Siempre HTTPS
const client = new LosCenotesClient({
apiKey: process.env.LOSCENOTES_API_KEY,
baseURL: "https://service-gateway.loscenotes.com", // Siempre usar HTTPS
timeout: 30000,
});
Validar Datos de Entrada
import Joi from "joi";
const reservationSchema = Joi.object({
cenoteId: Joi.string().uuid().required(),
date: Joi.date().min("now").required(),
visitors: Joi.number().integer().min(1).max(50).required(),
email: Joi.string().email().required(),
phone: Joi.string()
.pattern(/^\+?[1-9]\d{1,14}$/)
.required(),
});
async function createReservation(data: any) {
// Validar entrada antes de llamada API
const { error, value } = reservationSchema.validate(data);
if (error) {
throw new Error(`Validación falló: ${error.details[0].message}`);
}
return await client.reservations.create(value);
}
Sanitizar Datos
import DOMPurify from "isomorphic-dompurify";
function sanitizeReservationData(data: any) {
return {
...data,
guestName: DOMPurify.sanitize(data.guestName),
specialRequests: DOMPurify.sanitize(data.specialRequests),
// Sanitizar todas las cadenas proporcionadas por el usuario
};
}
Mejores Prácticas de Rendimiento
1. Estrategia de Caché
Implementar Caché Multi-Capa
class CacheManager {
private memoryCache = new Map<string, CacheItem>();
private redisClient: RedisClient;
async get<T>(key: string): Promise<T | null> {
// L1: Caché de memoria (más rápido)
const memoryItem = this.memoryCache.get(key);
if (memoryItem && !this.isExpired(memoryItem)) {
return memoryItem.data;
}
// L2: Caché Redis
const redisData = await this.redisClient.get(key);
if (redisData) {
const parsed = JSON.parse(redisData);
// Poblar caché de memoria
this.memoryCache.set(key, {
data: parsed,
expires: Date.now() + 300000, // 5 minutos
});
return parsed;
}
return null;
}
async set<T>(key: string, data: T, ttl: number = 3600): Promise<void> {
// Almacenar en ambos cachés
this.memoryCache.set(key, {
data,
expires: Date.now() + Math.min(ttl * 1000, 300000), // Máx 5 minutos en memoria
});
await this.redisClient.setex(key, ttl, JSON.stringify(data));
}
}
// Uso
const cache = new CacheManager();
async function getCenoteWithCache(cenoteId: string) {
const cacheKey = `cenote:${cenoteId}`;
// Intentar caché primero
let cenote = await cache.get(cacheKey);
if (!cenote) {
// Obtener de API
cenote = await client.cenotes.get(cenoteId);
// Cachear por 1 hora
await cache.set(cacheKey, cenote, 3600);
}
return cenote;
}
Estrategia de Invalidación de Caché
class SmartCacheManager extends CacheManager {
private dependencies = new Map<string, string[]>();
// Definir dependencias de caché
defineDependency(key: string, dependencies: string[]): void {
this.dependencies.set(key, dependencies);
}
async invalidate(pattern: string): Promise<void> {
const keys = await this.getMatchingKeys(pattern);
for (const key of keys) {
await this.delete(key);
// Invalidar cachés dependientes
const dependents = this.getDependents(key);
for (const dependent of dependents) {
await this.delete(dependent);
}
}
}
// Configurar dependencias
setupDependencies(): void {
this.defineDependency('cenotes:list', ['cenote:*']);
this.defineDependency('reservations:list', ['reservation:*']);
}
}
app.post('/webhooks/loscenotes', async (req, res) => {
const event = req.body;
switch (event.type) {
case 'cenote.updated':
await cacheManager.invalidate(`cenote:${event.data.id}`);
await cacheManager.invalidate('cenotes:list*');
break;
case 'reservation.updated':
await cacheManager.invalidate(`reservation:${event.data.id}`);
await cacheManager.invalidate('reservations:list*');
break;
}
res.status(200).json({ received: true });
});
2. Optimización de Solicitudes
Usar Endpoints de Lotes
// ❌ Ineficiente - Múltiples solicitudes
const cenotes = await Promise.all([
client.cenotes.get("id1"),
client.cenotes.get("id2"),
client.cenotes.get("id3"),
]);
// ✅ Eficiente - Solicitud de lote única
const cenotes = await client.cenotes.getBatch(["id1", "id2", "id3"]);
Implementar Deduplicación de Solicitudes
class RequestDeduplicator {
private pendingRequests = new Map<string, Promise<any>>();
async deduplicate<T>(key: string, request: () => Promise<T>): Promise<T> {
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key)!;
}
const promise = request().finally(() => {
// Limpiar después de que la solicitud se complete
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, promise);
return promise;
}
}
const deduplicator = new RequestDeduplicator();
// Múltiples llamadas al mismo recurso serán deduplicadas
const cenote1 = await deduplicator.deduplicate(`cenote:${cenoteId}`, () =>
client.cenotes.get(cenoteId)
);
const cenote2 = await deduplicator.deduplicate(`cenote:${cenoteId}`, () =>
client.cenotes.get(cenoteId)
); // Esto retornará la misma promesa que cenote1
Usar Paginación Sabiamente
// ✅ Bueno - Tamaño de página razonable
const cenotes = await client.cenotes.list({
page: 1,
perPage: 20, // Equilibrado entre rendimiento y frescura de datos
});
// ❌ Malo - Demasiados elementos
const cenotes = await client.cenotes.list({
page: 1,
perPage: 1000, // Respuesta lenta, alto uso de memoria
});
// ✅ Bueno - Paginación basada en cursor para conjuntos de datos grandes
async function getAllCenotes(): Promise<Cenote[]> {
const allCenotes: Cenote[] = [];
let cursor: string | undefined;
do {
const response = await client.cenotes.list({
cursor,
perPage: 50,
});
allCenotes.push(...response.data);
cursor = response.pagination.nextCursor;
} while (cursor);
return allCenotes;
}
3. Gestión de Conexiones
Usar Pooling de Conexiones
import axios from "axios";
import { Agent } from "https";
const httpsAgent = new Agent({
keepAlive: true,
maxSockets: 10,
maxFreeSockets: 5,
timeout: 30000,
});
const client = new LosCenotesClient({
apiKey: process.env.LOSCENOTES_API_KEY,
httpClient: axios.create({
httpsAgent,
timeout: 30000,
}),
});
Mejores Prácticas de Manejo de Errores
1. Manejo Integral de Errores
interface ErrorContext {
operation: string;
cenoteId?: string;
reservationId?: string;
userId?: string;
timestamp: Date;
}
class ErrorHandler {
async handleApiError(error: any, context: ErrorContext): Promise<void> {
const errorInfo = {
...context,
errorCode: error.code,
errorMessage: error.message,
statusCode: error.status,
requestId: error.requestId,
};
// Registrar error con contexto
logger.error("Error de API LosCenotes", errorInfo);
// Enviar a servicio de seguimiento de errores
await this.reportError(error, context);
// Manejar tipos de error específicos
switch (error.code) {
case "RATE_LIMIT_EXCEEDED":
await this.handleRateLimit(error, context);
break;
case "CENOTE_NOT_AVAILABLE":
await this.handleUnavailability(error, context);
break;
case "INSUFFICIENT_CAPACITY":
await this.handleCapacityIssue(error, context);
break;
default:
await this.handleGenericError(error, context);
break;
}
}
private async handleRateLimit(
error: any,
context: ErrorContext
): Promise<void> {
// Implementar retroceso exponencial
const delay = error.details.retryAfter * 1000;
await this.delay(delay);
// Notificar sistema de monitoreo
await this.sendAlert("RATE_LIMIT_HIT", { context, delay });
}
private async reportError(error: any, context: ErrorContext): Promise<void> {
// Enviar a Sentry, Bugsnag, etc.
Sentry.captureException(error, {
tags: {
operation: context.operation,
errorCode: error.code,
},
extra: context,
});
}
}
2. Patrón Circuit Breaker
class CircuitBreaker {
private failures = 0;
private lastFailureTime = 0;
private state: "CLOSED" | "OPEN" | "HALF_OPEN" = "CLOSED";
constructor(
private threshold: number = 5,
private timeout: number = 60000,
private monitor: (state: string) => void = () => {}
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === "OPEN") {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = "HALF_OPEN";
this.monitor("HALF_OPEN");
} else {
throw new Error("Circuit breaker está ABIERTO");
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failures = 0;
if (this.state === "HALF_OPEN") {
this.state = "CLOSED";
this.monitor("CLOSED");
}
}
private onFailure(): void {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = "OPEN";
this.monitor("OPEN");
}
}
}
// Uso
const circuitBreaker = new CircuitBreaker(5, 60000, (state) => {
console.log(`Estado del circuit breaker cambió a: ${state}`);
// Enviar alerta al sistema de monitoreo
});
const cenote = await circuitBreaker.execute(() => client.cenotes.get(cenoteId));
Monitoreo y Observabilidad
1. Registro Integral
class ApiLogger {
private logger: Logger;
constructor() {
this.logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: "api-errors.log",
level: "error",
}),
new winston.transports.File({ filename: "api-combined.log" }),
],
});
}
logRequest(method: string, url: string, headers: any): string {
const requestId = this.generateRequestId();
this.logger.info("Solicitud API", {
requestId,
method,
url,
headers: this.sanitizeHeaders(headers),
});
return requestId;
}
logResponse(
requestId: string,
status: number,
responseTime: number,
data?: any
): void {
this.logger.info("Respuesta API", {
requestId,
status,
responseTime,
dataSize: data ? JSON.stringify(data).length : 0,
});
}
logError(requestId: string, error: any): void {
this.logger.error("Error API", {
requestId,
error: {
code: error.code,
message: error.message,
status: error.status,
stack: error.stack,
},
});
}
private sanitizeHeaders(headers: any): any {
const sanitized = { ...headers };
if (sanitized.authorization) {
sanitized.authorization = "***CENSURADO***";
}
return sanitized;
}
private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
2. Recolección de Métricas
class MetricsCollector {
private metrics = {
requestsTotal: 0,
requestsSuccess: 0,
requestsError: 0,
responseTimesMs: [] as number[],
errorsByCode: {} as Record<string, number>,
};
recordRequest(
success: boolean,
responseTime: number,
errorCode?: string
): void {
this.metrics.requestsTotal++;
if (success) {
this.metrics.requestsSuccess++;
} else {
this.metrics.requestsError++;
if (errorCode) {
this.metrics.errorsByCode[errorCode] =
(this.metrics.errorsByCode[errorCode] || 0) + 1;
}
}
this.metrics.responseTimesMs.push(responseTime);
// Enviar métricas al servicio de monitoreo
this.sendMetrics();
}
getStats() {
const responseTimes = this.metrics.responseTimesMs;
return {
total: this.metrics.requestsTotal,
successRate: this.metrics.requestsSuccess / this.metrics.requestsTotal,
averageResponseTime:
responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length,
p95ResponseTime: this.percentile(responseTimes, 0.95),
errorsByCode: this.metrics.errorsByCode,
};
}
private percentile(arr: number[], p: number): number {
const sorted = arr.sort((a, b) => a - b);
const index = Math.ceil(sorted.length * p) - 1;
return sorted[index];
}
private sendMetrics(): void {
// Enviar a DataDog, New Relic, CloudWatch, etc.
if (this.metrics.requestsTotal % 100 === 0) {
const stats = this.getStats();
console.log("Métricas API:", stats);
}
}
}
3. Verificaciones de Salud
class HealthChecker {
async performHealthCheck(): Promise<HealthStatus> {
const checks = [
this.checkApiConnectivity(),
this.checkDatabaseConnection(),
this.checkRedisConnection(),
this.checkExternalServices(),
];
const results = await Promise.allSettled(checks);
const status: HealthStatus = {
overall: "healthy",
checks: {
api: this.getCheckResult(results[0]),
database: this.getCheckResult(results[1]),
redis: this.getCheckResult(results[2]),
external: this.getCheckResult(results[3]),
},
timestamp: new Date().toISOString(),
};
// Determinar salud general
const hasFailures = Object.values(status.checks).some(
(check) => check.status !== "healthy"
);
status.overall = hasFailures ? "degraded" : "healthy";
return status;
}
private async checkApiConnectivity(): Promise<void> {
const client = new LosCenotesClient({
apiKey: process.env.LOSCENOTES_API_KEY,
timeout: 5000,
});
await client.auth.verify();
}
private getCheckResult(result: PromiseSettledResult<any>): CheckResult {
if (result.status === "fulfilled") {
return { status: "healthy", message: "OK" };
} else {
return {
status: "unhealthy",
message: result.reason.message,
error: result.reason.code,
};
}
}
}
interface HealthStatus {
overall: "healthy" | "degraded" | "unhealthy";
checks: Record<string, CheckResult>;
timestamp: string;
}
interface CheckResult {
status: "healthy" | "unhealthy";
message: string;
error?: string;
}