Skip to Content

Adapters

Los adapters son interfaces que abstraen servicios externos y de terceros, permitiendo intercambiar implementaciones sin afectar la lógica de negocio. Su uso se reserva exclusivamente para servicios externos, no para comunicación entre módulos internos.

Concepto Clave

Los adapters NO se usan para comunicación interna. Los módulos se comunican entre sí mediante importación directa:

// ✅ Comunicación interna - Import directo import { usersService } from '@/modules/users/services'; // ✅ Servicio externo - Adapter import { emailAdapter } from './adapters/email-adapter';

Cuándo Usar Adapters

✅ Casos de Uso Correctos

  1. Servicios de Email: SendGrid, Resend, Mailgun, AWS SES
  2. Servicios de Pago: Stripe, PayPal, MercadoPago
  3. Servicios de Almacenamiento: AWS S3, Cloudinary, Supabase Storage
  4. Servicios de Autenticación: Auth0, Firebase Auth, Supabase Auth
  5. APIs de Terceros: Google Maps, Twilio, Slack
  6. Bases de Datos: PostgreSQL, MongoDB, Redis

❌ Casos Incorrectos

  1. Comunicación entre módulos internos
  2. Abstraer lógica de negocio propia
  3. Crear capas innecesarias de abstracción

Estructura de un Adapter

1. Definir la Interfaz

// src/modules/notifications/adapters/email-adapter.ts export interface EmailAdapter { sendEmail(params: SendEmailParams): Promise<EmailResult>; sendBulkEmail(params: BulkEmailParams): Promise<BulkEmailResult>; validateEmail(email: string): Promise<boolean>; } export interface SendEmailParams { to: string; subject: string; html: string; text?: string; attachments?: Attachment[]; } export interface EmailResult { success: boolean; messageId?: string; error?: string; }

2. Implementaciones Específicas

SendGrid Adapter

// src/modules/notifications/adapters/sendgrid-adapter.ts import sgMail from '@sendgrid/mail'; import { EmailAdapter, SendEmailParams, EmailResult } from './email-adapter'; export class SendGridAdapter implements EmailAdapter { constructor(private apiKey: string) { sgMail.setApiKey(apiKey); } async sendEmail(params: SendEmailParams): Promise<EmailResult> { try { const msg = { to: params.to, from: process.env.FROM_EMAIL!, subject: params.subject, html: params.html, text: params.text, }; const [response] = await sgMail.send(msg); return { success: true, messageId: response.headers['x-message-id'], }; } catch (error) { return { success: false, error: error.message, }; } } async sendBulkEmail(params: BulkEmailParams): Promise<BulkEmailResult> { // Implementación específica de SendGrid para emails masivos } async validateEmail(email: string): Promise<boolean> { // Usar API de validación de SendGrid return true; } }

Resend Adapter

// src/modules/notifications/adapters/resend-adapter.ts import { Resend } from 'resend'; import { EmailAdapter, SendEmailParams, EmailResult } from './email-adapter'; export class ResendAdapter implements EmailAdapter { private resend: Resend; constructor(apiKey: string) { this.resend = new Resend(apiKey); } async sendEmail(params: SendEmailParams): Promise<EmailResult> { try { const { data, error } = await this.resend.emails.send({ from: process.env.FROM_EMAIL!, to: params.to, subject: params.subject, html: params.html, text: params.text, }); if (error) { return { success: false, error: error.message, }; } return { success: true, messageId: data?.id, }; } catch (error) { return { success: false, error: error.message, }; } } async sendBulkEmail(params: BulkEmailParams): Promise<BulkEmailResult> { // Implementación específica de Resend } async validateEmail(email: string): Promise<boolean> { // Validación básica o usar servicio de Resend const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } }

3. Configuración del Adapter

// src/modules/notifications/config.ts import { EmailAdapter } from './adapters/email-adapter'; import { SendGridAdapter } from './adapters/sendgrid-adapter'; import { ResendAdapter } from './adapters/resend-adapter'; function createEmailAdapter(): EmailAdapter { const provider = process.env.EMAIL_PROVIDER; switch (provider) { case 'sendgrid': return new SendGridAdapter(process.env.SENDGRID_API_KEY!); case 'resend': return new ResendAdapter(process.env.RESEND_API_KEY!); default: throw new Error(`Email provider ${provider} not supported`); } } export const emailAdapter = createEmailAdapter();

4. Uso en el Servicio

Los adapters deben inyectarse en los servicios para mantener la flexibilidad y la testabilidad.

// src/modules/notifications/services.ts import { EmailAdapter } from './adapters/email-adapter'; import { emailAdapter } from './config'; // Importamos la instancia configurada del adapter export class NotificationService { constructor(private emailAdapter: EmailAdapter) {} async sendWelcomeEmail(userEmail: string, userName: string) { const result = await this.emailAdapter.sendEmail({ to: userEmail, subject: '¡Bienvenido a Creative Minds!', html: `<h1>Hola ${userName}</h1><p>Gracias por registrarte.</p>`, text: `Hola ${userName}, gracias por registrarte.`, }); if (!result.success) { throw new Error(`Failed to send welcome email: ${result.error}`); } return result; } async sendPasswordResetEmail(userEmail: string, resetToken: string) { const resetUrl = `${process.env.APP_URL}/reset-password?token=${resetToken}`; const result = await this.emailAdapter.sendEmail({ to: userEmail, subject: 'Restablecer contraseña', html: `<p>Haz clic <a href="${resetUrl}">aquí</a> para restablecer tu contraseña.</p>`, text: `Visita este enlace para restablecer tu contraseña: ${resetUrl}`, }); return result; } } // Exportamos una instancia del servicio con el adapter inyectado export const notificationService = new NotificationService(emailAdapter);

Ejemplo Completo: Payment Adapter

1. Interfaz del Payment Adapter

// src/modules/payments/adapters/payment-adapter.ts export interface PaymentAdapter { processPayment(params: ProcessPaymentParams): Promise<PaymentResult>; refundPayment(paymentId: string, amount?: number): Promise<RefundResult>; createCustomer(customerData: CustomerData): Promise<Customer>; getPaymentStatus(paymentId: string): Promise<PaymentStatus>; } export interface ProcessPaymentParams { amount: number; currency: string; customerId: string; paymentMethodId: string; description?: string; metadata?: Record<string, string>; } export interface PaymentResult { success: boolean; paymentId?: string; status?: 'succeeded' | 'pending' | 'failed'; error?: string; }

2. Stripe Adapter

// src/modules/payments/adapters/stripe-adapter.ts import Stripe from 'stripe'; import { PaymentAdapter, ProcessPaymentParams, PaymentResult } from './payment-adapter'; export class StripeAdapter implements PaymentAdapter { private stripe: Stripe; constructor(secretKey: string) { this.stripe = new Stripe(secretKey, { apiVersion: '2023-10-16', }); } async processPayment(params: ProcessPaymentParams): Promise<PaymentResult> { try { const paymentIntent = await this.stripe.paymentIntents.create({ amount: params.amount * 100, // Stripe usa centavos currency: params.currency, customer: params.customerId, payment_method: params.paymentMethodId, description: params.description, metadata: params.metadata, confirm: true, return_url: `${process.env.APP_URL}/payment/success`, }); return { success: paymentIntent.status === 'succeeded', paymentId: paymentIntent.id, status: paymentIntent.status as any, }; } catch (error) { return { success: false, error: error.message, }; } } async refundPayment(paymentId: string, amount?: number): Promise<RefundResult> { try { const refund = await this.stripe.refunds.create({ payment_intent: paymentId, amount: amount ? amount * 100 : undefined, }); return { success: refund.status === 'succeeded', refundId: refund.id, amount: refund.amount / 100, }; } catch (error) { return { success: false, error: error.message, }; } } async createCustomer(customerData: CustomerData): Promise<Customer> { const customer = await this.stripe.customers.create({ email: customerData.email, name: customerData.name, metadata: customerData.metadata, }); return { id: customer.id, email: customer.email!, name: customer.name!, }; } async getPaymentStatus(paymentId: string): Promise<PaymentStatus> { const paymentIntent = await this.stripe.paymentIntents.retrieve(paymentId); return { id: paymentIntent.id, status: paymentIntent.status as any, amount: paymentIntent.amount / 100, currency: paymentIntent.currency, }; } }

3. PayPal Adapter

// src/modules/payments/adapters/paypal-adapter.ts import { PaymentAdapter, ProcessPaymentParams, PaymentResult } from './payment-adapter'; export class PayPalAdapter implements PaymentAdapter { constructor( private clientId: string, private clientSecret: string, private environment: 'sandbox' | 'production' ) {} async processPayment(params: ProcessPaymentParams): Promise<PaymentResult> { try { // Implementación específica de PayPal const accessToken = await this.getAccessToken(); const payment = await fetch(`${this.getBaseUrl()}/v2/checkout/orders`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ intent: 'CAPTURE', purchase_units: [{ amount: { currency_code: params.currency.toUpperCase(), value: params.amount.toString(), }, description: params.description, }], }), }); const result = await payment.json(); return { success: result.status === 'CREATED', paymentId: result.id, status: result.status === 'CREATED' ? 'pending' : 'failed', }; } catch (error) { return { success: false, error: error.message, }; } } private async getAccessToken(): string { // Implementación para obtener token de PayPal const auth = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); const response = await fetch(`${this.getBaseUrl()}/v1/oauth2/token`, { method: 'POST', headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'grant_type=client_credentials', }); const data = await response.json(); return data.access_token; } private getBaseUrl(): string { return this.environment === 'sandbox' ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'; } // Implementar otros métodos... }

3. Configuración y Uso

// src/modules/payments/config.ts import { PaymentAdapter } from './adapters/payment-adapter'; import { StripeAdapter } from './adapters/stripe-adapter'; import { PayPalAdapter } from './adapters/paypal-adapter'; function createPaymentAdapter(): PaymentAdapter { const provider = process.env.PAYMENT_PROVIDER; switch (provider) { case 'stripe': return new StripeAdapter(process.env.STRIPE_SECRET_KEY!); case 'paypal': return new PayPalAdapter( process.env.PAYPAL_CLIENT_ID!, process.env.PAYPAL_CLIENT_SECRET!, process.env.NODE_ENV === 'production' ? 'production' : 'sandbox' ); default: throw new Error(`Payment provider ${provider} not supported`); } } export const paymentAdapter = createPaymentAdapter();
// src/modules/payments/services.ts import { paymentAdapter } from './config'; export const paymentsService = { async processPayment(paymentData: ProcessPaymentData) { // Validaciones de negocio if (paymentData.amount <= 0) { throw new Error('Amount must be greater than 0'); } // Usar el adapter const result = await paymentAdapter.processPayment({ amount: paymentData.amount, currency: paymentData.currency, customerId: paymentData.customerId, paymentMethodId: paymentData.paymentMethodId, description: `Payment for order ${paymentData.orderId}`, metadata: { orderId: paymentData.orderId, userId: paymentData.userId, }, }); if (!result.success) { throw new Error(`Payment failed: ${result.error}`); } return result; }, };

Beneficios de los Adapters

1. Intercambiabilidad

Cambiar de proveedor sin tocar la lógica de negocio:

// Solo cambiar la configuración // De Stripe a PayPal sin tocar servicios PAYMENT_PROVIDER=paypal # Era stripe

2. Testabilidad

Crear mocks fácilmente:

// src/modules/payments/adapters/mock-payment-adapter.ts export class MockPaymentAdapter implements PaymentAdapter { async processPayment(params: ProcessPaymentParams): Promise<PaymentResult> { return { success: true, paymentId: 'mock-payment-id', status: 'succeeded', }; } // ... otros métodos mock }

3. Flexibilidad

Diferentes implementaciones para diferentes entornos:

function createPaymentAdapter(): PaymentAdapter { if (process.env.NODE_ENV === 'test') { return new MockPaymentAdapter(); } if (process.env.NODE_ENV === 'development') { return new StripeAdapter(process.env.STRIPE_TEST_KEY!); } return new StripeAdapter(process.env.STRIPE_LIVE_KEY!); }

Principios de los Adapters

1. Solo para Servicios Externos

No crear adapters para lógica interna de la aplicación.

2. Interfaces Claras

Definir contratos bien documentados y tipados.

3. Configuración Centralizada

Un solo lugar para cambiar implementaciones.

4. Manejo de Errores Consistente

Todos los adapters deben manejar errores de forma similar.

5. Abstracción Apropiada

No sobre-abstraer. La interfaz debe ser útil, no genérica en exceso.

Los adapters son una herramienta poderosa cuando se usan correctamente: para abstraer servicios externos y permitir flexibilidad en las implementaciones sin comprometer la simplicidad de la arquitectura interna.

Last updated on