Custom Adapter Development
Creating custom adapters for Mifty Framework to integrate with third-party services and APIs.
Overview
Mifty adapters are reusable modules that provide standardized interfaces for integrating with external services. They follow a consistent pattern and can be easily shared across projects or published to npm.
Adapter Architecture
Core Components
Every Mifty adapter consists of:
- Configuration Schema - Defines required and optional settings
- Service Interface - Standardized methods for the service type
- Implementation - Actual service integration logic
- Type Definitions - TypeScript interfaces and types
- Tests - Unit and integration tests
Adapter Types
Mifty supports several adapter categories:
- Authentication - OAuth, SAML, JWT providers
- Storage - File storage, CDN, cloud storage
- Communication - Email, SMS, push notifications
- Payment - Payment processors, billing systems
- Analytics - Tracking, metrics, monitoring
- Database - Additional database providers
- Cache - Redis, Memcached, in-memory stores
Creating Your First Adapter
1. Generate Adapter Scaffold
# Create a new adapter
mifty adapter:create my-service-adapter --type=communication
# Or use the interactive generator
mifty adapter:create --interactive
This generates the following structure:
src/adapters/my-service-adapter/
├── index.ts # Main export
├── config.ts # Configuration schema
├── service.ts # Service implementation
├── types.ts # Type definitions
├── __tests__/ # Test files
│ ├── service.test.ts
│ └── integration.test.ts
└── README.md # Adapter documentation
2. Define Configuration Schema
// src/adapters/my-service-adapter/config.ts
import { z } from 'zod';
import { AdapterConfig } from '@mifty/core';
export const MyServiceConfigSchema = z.object({
apiKey: z.string().min(1, 'API key is required'),
apiUrl: z.string().url().default('https://api.myservice.com'),
timeout: z.number().positive().default(5000),
retryAttempts: z.number().min(0).default(3),
enableLogging: z.boolean().default(false)
});
export type MyServiceConfig = z.infer<typeof MyServiceConfigSchema>;
export const myServiceAdapterConfig: AdapterConfig<MyServiceConfig> = {
name: 'my-service',
version: '1.0.0',
description: 'Integration with MyService API',
configSchema: MyServiceConfigSchema,
category: 'communication',
tags: ['email', 'notifications', 'api']
};
3. Define Service Interface
// src/adapters/my-service-adapter/types.ts
export interface SendMessageRequest {
to: string;
subject?: string;
content: string;
template?: string;
variables?: Record<string, any>;
}
export interface SendMessageResponse {
messageId: string;
status: 'sent' | 'queued' | 'failed';
timestamp: Date;
}
export interface MessageStatus {
messageId: string;
status: 'sent' | 'delivered' | 'failed' | 'bounced';
deliveredAt?: Date;
error?: string;
}
export interface MyServiceAdapter {
sendMessage(request: SendMessageRequest): Promise<SendMessageResponse>;
getMessageStatus(messageId: string): Promise<MessageStatus>;
validateConfig(): Promise<boolean>;
}
4. Implement Service Logic
// src/adapters/my-service-adapter/service.ts
import { Logger } from '@mifty/core';
import { MyServiceConfig } from './config';
import {
MyServiceAdapter,
SendMessageRequest,
SendMessageResponse,
MessageStatus
} from './types';
export class MyServiceAdapterImpl implements MyServiceAdapter {
private readonly logger: Logger;
private readonly httpClient: any; // Use your preferred HTTP client
constructor(
private readonly config: MyServiceConfig,
logger?: Logger
) {
this.logger = logger || new Logger('MyServiceAdapter');
this.httpClient = this.createHttpClient();
}
async sendMessage(request: SendMessageRequest): Promise<SendMessageResponse> {
try {
this.logger.info('Sending message', { to: request.to });
const response = await this.httpClient.post('/messages', {
recipient: request.to,
subject: request.subject,
body: request.content,
template_id: request.template,
variables: request.variables
});
return {
messageId: response.data.id,
status: response.data.status,
timestamp: new Date(response.data.created_at)
};
} catch (error) {
this.logger.error('Failed to send message', error);
throw new Error(`Failed to send message: ${error.message}`);
}
}
async getMessageStatus(messageId: string): Promise<MessageStatus> {
try {
const response = await this.httpClient.get(`/messages/${messageId}`);
return {
messageId: response.data.id,
status: response.data.status,
deliveredAt: response.data.delivered_at ?
new Date(response.data.delivered_at) : undefined,
error: response.data.error_message
};
} catch (error) {
this.logger.error('Failed to get message status', error);
throw new Error(`Failed to get message status: ${error.message}`);
}
}
async validateConfig(): Promise<boolean> {
try {
await this.httpClient.get('/health');
return true;
} catch (error) {
this.logger.error('Config validation failed', error);
return false;
}
}
private createHttpClient() {
// Configure HTTP client with authentication, timeouts, etc.
return {
baseURL: this.config.apiUrl,
timeout: this.config.timeout,
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
},
// Add retry logic, error handling, etc.
};
}
}
5. Create Main Export
// src/adapters/my-service-adapter/index.ts
import { AdapterFactory } from '@mifty/core';
import { MyServiceAdapterImpl } from './service';
import { myServiceAdapterConfig, MyServiceConfig } from './config';
export const createMyServiceAdapter: AdapterFactory<MyServiceConfig> = (config) => {
return new MyServiceAdapterImpl(config);
};
export {
myServiceAdapterConfig,
MyServiceConfig,
MyServiceAdapter,
SendMessageRequest,
SendMessageResponse,
MessageStatus
} from './types';
// Default export for easy importing
export default {
config: myServiceAdapterConfig,
factory: createMyServiceAdapter
};
Advanced Features
Error Handling and Retry Logic
import { RetryPolicy, CircuitBreaker } from '@mifty/core';
export class RobustMyServiceAdapter extends MyServiceAdapterImpl {
private readonly retryPolicy: RetryPolicy;
private readonly circuitBreaker: CircuitBreaker;
constructor(config: MyServiceConfig) {
super(config);
this.retryPolicy = new RetryPolicy({
maxAttempts: config.retryAttempts,
backoffStrategy: 'exponential',
baseDelay: 1000
});
this.circuitBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000
});
}
async sendMessage(request: SendMessageRequest): Promise<SendMessageResponse> {
return this.circuitBreaker.execute(async () => {
return this.retryPolicy.execute(async () => {
return super.sendMessage(request);
});
});
}
}
Caching Support
import { CacheManager } from '@mifty/core';
export class CachedMyServiceAdapter extends MyServiceAdapterImpl {
constructor(
config: MyServiceConfig,
private readonly cache: CacheManager
) {
super(config);
}
async getMessageStatus(messageId: string): Promise<MessageStatus> {
const cacheKey = `message_status:${messageId}`;
// Try cache first
const cached = await this.cache.get<MessageStatus>(cacheKey);
if (cached) {
return cached;
}
// Fetch from API
const status = await super.getMessageStatus(messageId);
// Cache for 5 minutes
await this.cache.set(cacheKey, status, 300);
return status;
}
}
Event Emission
import { EventEmitter } from '@mifty/core';
export class EventAwareMyServiceAdapter extends MyServiceAdapterImpl {
constructor(
config: MyServiceConfig,
private readonly eventEmitter: EventEmitter
) {
super(config);
}
async sendMessage(request: SendMessageRequest): Promise<SendMessageResponse> {
this.eventEmitter.emit('message.sending', { request });
try {
const response = await super.sendMessage(request);
this.eventEmitter.emit('message.sent', { request, response });
return response;
} catch (error) {
this.eventEmitter.emit('message.failed', { request, error });
throw error;
}
}
}
Testing Your Adapter
Unit Tests
// src/adapters/my-service-adapter/__tests__/service.test.ts
import { MyServiceAdapterImpl } from '../service';
import { MyServiceConfig } from '../config';
describe('MyServiceAdapter', () => {
let adapter: MyServiceAdapterImpl;
let mockConfig: MyServiceConfig;
beforeEach(() => {
mockConfig = {
apiKey: 'test-key',
apiUrl: 'https://api.test.com',
timeout: 5000,
retryAttempts: 3,
enableLogging: false
};
adapter = new MyServiceAdapterImpl(mockConfig);
});
describe('sendMessage', () => {
it('should send message successfully', async () => {
// Mock HTTP client response
jest.spyOn(adapter as any, 'httpClient').mockResolvedValue({
data: {
id: 'msg-123',
status: 'sent',
created_at: '2023-01-01T00:00:00Z'
}
});
const request = {
to: 'test@example.com',
subject: 'Test',
content: 'Hello World'
};
const response = await adapter.sendMessage(request);
expect(response.messageId).toBe('msg-123');
expect(response.status).toBe('sent');
});
it('should handle API errors', async () => {
jest.spyOn(adapter as any, 'httpClient').mockRejectedValue(
new Error('API Error')
);
const request = {
to: 'test@example.com',
content: 'Hello World'
};
await expect(adapter.sendMessage(request)).rejects.toThrow('Failed to send message');
});
});
});
Integration Tests
// src/adapters/my-service-adapter/__tests__/integration.test.ts
import { MyServiceAdapterImpl } from '../service';
describe('MyServiceAdapter Integration', () => {
let adapter: MyServiceAdapterImpl;
beforeAll(() => {
// Use test API credentials
adapter = new MyServiceAdapterImpl({
apiKey: process.env.TEST_API_KEY!,
apiUrl: 'https://api-sandbox.myservice.com',
timeout: 10000,
retryAttempts: 1,
enableLogging: true
});
});
it('should validate configuration', async () => {
const isValid = await adapter.validateConfig();
expect(isValid).toBe(true);
});
it('should send and track message', async () => {
const response = await adapter.sendMessage({
to: 'test@example.com',
subject: 'Integration Test',
content: 'This is a test message'
});
expect(response.messageId).toBeDefined();
expect(response.status).toMatch(/sent|queued/);
// Check status
const status = await adapter.getMessageStatus(response.messageId);
expect(status.messageId).toBe(response.messageId);
});
});
Publishing Your Adapter
1. Package Configuration
// package.json
{
"name": "@your-org/mifty-adapter-myservice",
"version": "1.0.0",
"description": "Mifty adapter for MyService integration",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": ["mifty", "adapter", "myservice", "communication"],
"peerDependencies": {
"@mifty/core": "^1.0.0"
},
"files": ["dist", "README.md"]
}
2. Documentation
Create comprehensive README.md:
# Mifty MyService Adapter
Integration adapter for MyService API.
## Installation
\`\`\`bash
npm install @your-org/mifty-adapter-myservice
\`\`\`
## Configuration
\`\`\`typescript
import { myServiceAdapter } from '@your-org/mifty-adapter-myservice';
const adapter = myServiceAdapter.factory({
apiKey: process.env.MYSERVICE_API_KEY,
apiUrl: 'https://api.myservice.com',
timeout: 5000
});
\`\`\`
## Usage
\`\`\`typescript
const response = await adapter.sendMessage({
to: 'user@example.com',
subject: 'Welcome!',
content: 'Welcome to our service!'
});
\`\`\`
3. Publish to Registry
# Build the adapter
npm run build
# Run tests
npm test
# Publish to npm
npm publish
# Or publish to Mifty adapter registry
mifty adapter:publish
Best Practices
Configuration Management
- Use environment variables for sensitive data
- Provide sensible defaults
- Validate configuration on startup
- Support multiple environments
Error Handling
- Use specific error types
- Provide meaningful error messages
- Implement proper retry logic
- Log errors appropriately
Performance
- Implement connection pooling
- Use caching where appropriate
- Add circuit breakers for external calls
- Monitor and measure performance
Security
- Never log sensitive data
- Validate all inputs
- Use secure communication (HTTPS)
- Follow principle of least privilege
This comprehensive guide should help you create robust, reusable adapters that integrate seamlessly with the Mifty ecosystem.