Documentation
Providers

Providers Overview

Providers are the data layer for all ECC (Elixir Cloud Components) packages. They abstract the communication with various GA4GH APIs and services, allowing components to work with different data sources while maintaining a consistent interface.

How it works: Create a provider instance with your API endpoint, pass it to any compatible component, and the component handles all data fetching automatically.

How Providers Work

Architecture Overview

Basic Pattern

All ECC providers follow the same pattern:

  1. Create a provider with your API endpoint
  2. Pass it to components via props
  3. Components automatically fetch data using the provider
  4. Handle loading states and errors in your UI
// 1. Create provider
const provider = new RestProvider('https://api.example.com');
 
// 2. Pass to component
<Component provider={provider} />
 
// 3. Component uses provider internally
// 4. Handle events/states as needed

Common Use Cases

Environment-Based Configuration

Manage different API endpoints across environments:

// hooks/useProviders.js
import { useMemo } from 'react';
 
export function useProviders() {
  return useMemo(() => {
    const env = process.env.NODE_ENV || 'development';
    const baseUrls = {
      development: 'http://localhost:8080',
      staging: 'https://staging-api.example.com',
      production: 'https://api.example.com'
    };
 
    return {
      serviceRegistry: new RestServiceRegistryProvider(`${baseUrls[env]}/ga4gh/registry/v1`),
      wes: new RestWesProvider(`${baseUrls[env]}/ga4gh/wes/v1`),
      trs: new RestTrsProvider(`${baseUrls[env]}/ga4gh/trs/v2`)
    };
  }, []);
}
 
// Usage in components
function MyApp() {
  const providers = useProviders();
  return <ServiceList provider={providers.serviceRegistry} />;
}

Authentication Patterns

Components don't handle authentication; your app does. Every REST provider sends all of its requests through a fetch(input, init) method. Override it in a subclass to add headers such as Authorization, so you can use any auth flow (OAuth/OIDC, API keys, ...). You can also implement the provider interface yourself.

⚠️

Build headers inside fetch, so that every request gets a current token. Don't set Content-Type: providers set it where needed, and file uploads (e.g. WES runWorkflow) need the browser to set it.

API Key Authentication

class ApiKeyServiceRegistryProvider extends RestServiceRegistryProvider {
  constructor(baseUrl, apiKey) {
    super(baseUrl);
    this.apiKey = apiKey;
  }
 
  fetch(input, init = {}) {
    const headers = new Headers(init.headers);
    headers.set('X-API-Key', this.apiKey);
    return super.fetch(input, { ...init, headers });
  }
}
 
// Usage
const provider = new ApiKeyServiceRegistryProvider('https://api.example.com', 'your-api-key');
⚠️

Anyone can read an API key that ships to the browser. Only use keys that are meant to be public, or add the key in a backend proxy instead.

Bearer Token Authentication

class TokenWesProvider extends RestWesProvider {
  constructor(baseUrl, getToken) {
    super(baseUrl);
    this.getToken = getToken;
  }
 
  async fetch(input, init = {}) {
    const headers = new Headers(init.headers);
    // Requested for every call, so refreshed tokens are used
    headers.set('Authorization', `Bearer ${await this.getToken()}`);
    return super.fetch(input, { ...init, headers });
  }
}
 
// Usage with OAuth
const wesProvider = new TokenWesProvider(
  'https://wes.example.com/ga4gh/wes/v1',
  () => oauth.getAccessToken()
);

Custom Headers

// Add headers to every request of any REST provider
function withHeaders(Provider, getHeaders) {
  return class extends Provider {
    async fetch(input, init = {}) {
      const headers = new Headers(init.headers);
      Object.entries(await getHeaders()).forEach(([name, value]) => {
        headers.set(name, value);
      });
      return super.fetch(input, { ...init, headers });
    }
  };
}
 
// Usage: header values must be strings, so compute them per request
const OrganizationalProvider = withHeaders(RestServiceRegistryProvider, () => ({
  'X-Organization': 'my-org',
  'X-Client-Version': '1.0.0',
  'X-Request-ID': generateRequestId()
}));
const provider = new OrganizationalProvider('https://api.example.com');

Error Handling Patterns

Centralized Error Handling

// Global error handler
class ErrorHandlingProvider {
  constructor(baseProvider, errorHandler) {
    this.baseProvider = baseProvider;
    this.errorHandler = errorHandler;
  }
 
  // Wrap all methods with error handling
  async wrapMethod(methodName, ...args) {
    try {
      return await this.baseProvider[methodName](...args);
    } catch (error) {
      return this.errorHandler(error, methodName, args);
    }
  }
}
 
// Error handler function
function handleProviderError(error, method, args) {
  console.error(`Provider error in ${method}:`, error);
  
  // Log to monitoring service
  analytics.track('provider_error', {
    method,
    error: error.message,
    endpoint: args[0]
  });
  
  // Transform error for user
  if (error.message.includes('404')) {
    throw new Error('Resource not found. Please check your configuration.');
  } else if (error.message.includes('401')) {
    throw new Error('Authentication failed. Please check your credentials.');
  } else if (error.message.includes('network')) {
    throw new Error('Network error. Please check your connection.');
  }
  
  throw error; // Re-throw if no specific handling
}

Retry Logic

// Retry wrapper for transient failures
function withRetry(Provider, maxRetries = 3, delay = 1000) {
  return class extends Provider {
    async fetch(url, options = {}) {
      let lastError;
      
      for (let i = 0; i <= maxRetries; i++) {
        try {
          return await super.fetch(url, options);
        } catch (error) {
          lastError = error;
          
          // Don't retry on client errors
          if (error.status >= 400 && error.status < 500) {
            throw error;
          }
          
          if (i < maxRetries) {
            await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
          }
        }
      }
      
      throw lastError;
    }
  };
}
 
// Usage
const RetryableProvider = withRetry(RestWesProvider, 3, 1000);
const provider = new RetryableProvider('https://api.example.com');

Caching and Performance

Request Caching

// Simple memory cache
class CachedProvider {
  constructor(baseProvider, ttl = 300000) { // 5 minutes default
    this.baseProvider = baseProvider;
    this.cache = new Map();
    this.ttl = ttl;
  }
 
  generateCacheKey(method, args) {
    return `${method}:${JSON.stringify(args)}`;
  }
 
  async cachedRequest(method, ...args) {
    const key = this.generateCacheKey(method, args);
    const cached = this.cache.get(key);
    
    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.data;
    }
    
    const data = await this.baseProvider[method](...args);
    this.cache.set(key, { data, timestamp: Date.now() });
    
    return data;
  }
 
  // Cache service info (rarely changes)
  async getServiceInfo() {
    return this.cachedRequest('getServiceInfo');
  }
 
  // Don't cache user-specific data
  async getServices() {
    return this.baseProvider.getServices();
  }
}

Request Deduplication

// Prevent duplicate concurrent requests
class DeduplicatedProvider {
  constructor(baseProvider) {
    this.baseProvider = baseProvider;
    this.pendingRequests = new Map();
  }
 
  async deduplicatedRequest(method, ...args) {
    const key = `${method}:${JSON.stringify(args)}`;
    
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key);
    }
    
    const promise = this.baseProvider[method](...args)
      .finally(() => this.pendingRequests.delete(key));
    
    this.pendingRequests.set(key, promise);
    return promise;
  }
}

Testing Patterns

Mock Providers

// Mock provider for testing
export class MockServiceRegistryProvider {
  constructor(mockData = {}) {
    this.mockData = {
      services: [],
      serviceInfo: { name: 'Mock Registry' },
      ...mockData
    };
  }
 
  async getServices() {
    // Simulate network delay
    await new Promise(resolve => setTimeout(resolve, 100));
    return this.mockData.services;
  }
 
  async getServiceById(id) {
    await new Promise(resolve => setTimeout(resolve, 100));
    return this.mockData.services.find(s => s.id === id);
  }
 
  async getServiceInfo() {
    await new Promise(resolve => setTimeout(resolve, 50));
    return this.mockData.serviceInfo;
  }
 
  // Add method to update mock data during tests
  updateMockData(newData) {
    this.mockData = { ...this.mockData, ...newData };
  }
}
 
// Usage in tests
const mockProvider = new MockServiceRegistryProvider({
  services: [
    { id: 'service-1', name: 'Test Service 1' },
    { id: 'service-2', name: 'Test Service 2' }
  ]
});

Provider Testing Utilities

// Test utilities for any provider
export class ProviderTester {
  constructor(provider) {
    this.provider = provider;
    this.results = [];
  }
 
  async testMethod(methodName, ...args) {
    const startTime = Date.now();
    
    try {
      const result = await this.provider[methodName](...args);
      const duration = Date.now() - startTime;
      
      this.results.push({
        method: methodName,
        status: 'success',
        duration,
        result: result ? 'data received' : 'no data'
      });
      
      return result;
    } catch (error) {
      const duration = Date.now() - startTime;
      
      this.results.push({
        method: methodName,
        status: 'error',
        duration,
        error: error.message
      });
      
      throw error;
    }
  }
 
  getTestResults() {
    return this.results;
  }
 
  async runHealthCheck() {
    try {
      await this.testMethod('getServiceInfo');
      console.log('✅ Provider health check passed');
      return true;
    } catch (error) {
      console.error('❌ Provider health check failed:', error.message);
      return false;
    }
  }
}
 
// Usage
const tester = new ProviderTester(myProvider);
await tester.runHealthCheck();
console.table(tester.getTestResults());

Multi-Provider Applications

Provider Context

// React context for managing multiple providers
import { createContext, useContext } from 'react';
 
const ProviderContext = createContext();
 
export function ProviderProvider({ children, providers }) {
  return (
    <ProviderContext.Provider value={providers}>
      {children}
    </ProviderContext.Provider>
  );
}
 
export function useProvider(type) {
  const providers = useContext(ProviderContext);
  if (!providers || !providers[type]) {
    throw new Error(`Provider ${type} not found`);
  }
  return providers[type];
}
 
// Usage
function App() {
  const providers = {
    serviceRegistry: new RestServiceRegistryProvider('https://registry.example.com'),
    wes: new RestWesProvider('https://wes.example.com'),
    trs: new RestTrsProvider('https://trs.example.com')
  };
 
  return (
    <ProviderProvider providers={providers}>
      <ServiceList />
      <WorkflowSubmitter />
      <ToolBrowser />
    </ProviderProvider>
  );
}
 
function ServiceList() {
  const provider = useProvider('serviceRegistry');
  return <EccClientGa4ghServiceRegistryServices provider={provider} />;
}

Provider Composition

// Compose multiple providers for complex workflows
class CompositeWorkflowProvider {
  constructor(serviceRegistryProvider, wesProvider, trsProvider) {
    this.serviceRegistry = serviceRegistryProvider;
    this.wes = wesProvider;
    this.trs = trsProvider;
  }
 
  // High-level workflow operations
  async discoverAndRunWorkflow(workflowQuery) {
    // 1. Find workflow in TRS
    const tools = await this.trs.getToolsList(10, 0, {}, workflowQuery);
    const workflow = tools.find(t => t.toolclass.name === 'Workflow');
    
    if (!workflow) {
      throw new Error('Workflow not found');
    }
 
    // 2. Find compatible WES service
    const services = await this.serviceRegistry.getServices();
    const wesService = services.find(s => 
      s.type.artifact === 'wes' && 
      s.organization.name === workflow.organization
    );
    
    if (!wesService) {
      throw new Error('No compatible WES service found');
    }
 
    // 3. Submit workflow
    const runRequest = {
      workflow_url: workflow.url,
      workflow_type: workflow.versions[0].descriptor_type[0],
      workflow_params: {}
    };
    
    return await this.wes.runWorkflow(runRequest);
  }
}

Best Practices

1. Provider Lifecycle

// Proper provider lifecycle management
function useProviderLifecycle(createProvider) {
  const [provider, setProvider] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    let cancelled = false;
 
    async function initProvider() {
      try {
        setLoading(true);
        const newProvider = await createProvider();
        
        if (!cancelled) {
          setProvider(newProvider);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }
 
    initProvider();
 
    return () => {
      cancelled = true;
      // Cleanup provider if needed
      if (provider?.cleanup) {
        provider.cleanup();
      }
    };
  }, [createProvider]);
 
  return { provider, loading, error };
}

2. Error Boundaries

// Error boundary for provider failures
class ProviderErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
 
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
 
  componentDidCatch(error, errorInfo) {
    console.error('Provider error:', error, errorInfo);
    
    // Report to error tracking service
    if (this.props.onError) {
      this.props.onError(error, errorInfo);
    }
  }
 
  render() {
    if (this.state.hasError) {
      return (
        <div className="provider-error">
          <h3>Connection Error</h3>
          <p>Unable to connect to the service. Please check your configuration.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Retry
          </button>
        </div>
      );
    }
 
    return this.props.children;
  }
}

3. Performance Monitoring

// Monitor provider performance
class MonitoredProvider {
  constructor(baseProvider, onMetrics) {
    this.baseProvider = baseProvider;
    this.onMetrics = onMetrics;
  }
 
  async wrapMethod(methodName, ...args) {
    const startTime = performance.now();
    const requestId = generateId();
    
    try {
      const result = await this.baseProvider[methodName](...args);
      const duration = performance.now() - startTime;
      
      this.onMetrics({
        requestId,
        method: methodName,
        duration,
        status: 'success',
        timestamp: Date.now()
      });
      
      return result;
    } catch (error) {
      const duration = performance.now() - startTime;
      
      this.onMetrics({
        requestId,
        method: methodName,
        duration,
        status: 'error',
        error: error.message,
        timestamp: Date.now()
      });
      
      throw error;
    }
  }
}

Next Steps

  • Package-specific documentation: See individual provider documentation for REST implementations and interfaces:

  • Component documentation: Learn how to use providers with specific components in each package's component documentation

  • Examples: Check the demo applications in each package for complete implementation examples

Pro tip: Start with the basic REST provider from the package you need, then apply the patterns from this guide to add authentication, error handling, and other features as needed.

Elixir Cloud & AAICloud SDKElixir

Released under Apache 2.0 License.

Copyright © 2023-2026 ELIXIR