Plugin System

Extend Apisto's functionality with custom plugins and built-in extensions

Extensible 15 min read

Apisto Plugin System

Apisto's plugin system allows you to extend its functionality with custom behaviors, interceptors, and utilities. Plugins can modify requests, handle responses, add caching, logging, and much more.

🔌 Powerful Extensibility: Plugins can hook into Apisto's request lifecycle at multiple points, giving you complete control over API interactions.

Built-in Plugins

Apisto comes with several useful built-in plugins that you can enable with a single line of code.

📝 Logger Plugin

Automatically logs all requests and responses for debugging and monitoring.


// Enable logger plugin
const api = new Apisto({
  baseURL: 'https://api.example.com',
  enableLogging: true // Enabled by default
});

// Or manually register
api.use({
  name: 'custom-logger',
  install(apisto) {
    apisto.addRequestInterceptor(async (options, url) => {
      console.log(`📤 [${options.method}] ${url}`, options);
      return options;
    });

    apisto.addResponseInterceptor(async (response, options) => {
      console.log(`📥 [${response.status}] ${options.url}`);
      return response;
    });

    apisto.addErrorInterceptor(async (error, options) => {
      console.error(`❌ [ERROR] ${options.method} ${options.url}`, error);
      return error;
    });
  }
});
            

💾 Cache Plugin

Automatic caching of GET requests with configurable TTL and cache invalidation.


// Enable cache plugin
const api = new Apisto({
  baseURL: 'https://api.example.com',
  enableCache: true, // Enabled by default
  cacheTTL: 300000 // 5 minutes
});

// Usage with cache control
async function fetchData() {
  // This request will be cached
  const data = await api.get('/users', {
    cacheTTL: 60000 // Override global TTL
  });
  
  // Skip cache for this request
  const freshData = await api.get('/users', {
    skipCache: true
  });
  
  return data;
}

// Manual cache management
function clearCache() {
  // Custom cache clearing logic
  const cacheKeys = Object.keys(localStorage)
    .filter(key => key.startsWith('apisto-cache-'));
  
  cacheKeys.forEach(key => localStorage.removeItem(key));
}
            

Creating Custom Plugins

Build your own plugins to add custom functionality to Apisto. Plugins can implement various lifecycle hooks.


// Basic plugin structure
const MyCustomPlugin = {
  name: 'my-custom-plugin',
  
  // Required: install method
  install(apisto) {
    console.log('MyCustomPlugin installed!');
    
    // Add request interceptor
    apisto.addRequestInterceptor(async (options, url) => {
      // Add custom header to all requests
      options.headers['X-My-Custom-Header'] = 'plugin-value';
      return options;
    });
    
    // Add response interceptor
    apisto.addResponseInterceptor(async (response, options) => {
      // Process all responses
      const data = await response.clone().json();
      
      // Add custom processing
      if (data.metadata) {
        data.processedByPlugin = true;
        data.processedAt = new Date().toISOString();
      }
      
      // Return new response with processed data
      return new Response(JSON.stringify(data), {
        status: response.status,
        headers: response.headers
      });
    });
    
    // Add error interceptor
    apisto.addErrorInterceptor(async (error, options) => {
      // Custom error handling
      if (error.status === 429) {
        // Implement custom retry logic for rate limits
        console.warn('Rate limited, implementing custom backoff...');
        await new Promise(resolve => setTimeout(resolve, 5000));
        return apisto.request(options.url, options);
      }
      return error;
    });
    
    // Add custom methods to apisto instance
    apisto.customMethod = function() {
      console.log('Custom method called!');
      return this;
    };
  }
};

// Register the plugin
const api = new Apisto({
  baseURL: 'https://api.example.com'
});

api.use(MyCustomPlugin);

// Now you can use the custom method
api.customMethod();
        

Advanced Plugin Examples

Real-world plugin examples for common use cases.

🔐 Authentication Plugin


const AuthPlugin = {
  name: 'auth-plugin',
  
  install(apisto) {
    let authToken = null;
    let refreshToken = null;
    let refreshPromise = null;
    
    // Store auth methods
    apisto.setAuthToken = function(token) {
      authToken = token;
      return this;
    };
    
    apisto.setRefreshToken = function(token) {
      refreshToken = token;
      return this;
    };
    
    // Add auth header to all requests
    apisto.addRequestInterceptor(async (options) => {
      if (authToken) {
        options.headers.Authorization = `Bearer ${authToken}`;
      }
      return options;
    });
    
    // Handle token refresh on 401
    apisto.addErrorInterceptor(async (error, options) => {
      if (error.status === 401 && refreshToken && !options._retry) {
        if (!refreshPromise) {
          refreshPromise = refreshAuthToken(refreshToken)
            .finally(() => { refreshPromise = null; });
        }
        
        const newTokens = await refreshPromise;
        authToken = newTokens.accessToken;
        refreshToken = newTokens.refreshToken;
        
        // Retry original request with new token
        options._retry = true;
        options.headers.Authorization = `Bearer ${authToken}`;
        return apisto.request(options.url, options);
      }
      return error;
    });
    
    async function refreshAuthToken(token) {
      const response = await fetch('/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken: token })
      });
      
      if (!response.ok) {
        throw new Error('Token refresh failed');
      }
      
      return response.json();
    }
  }
};
            

📊 Analytics Plugin


const AnalyticsPlugin = {
  name: 'analytics-plugin',
  
  install(apisto) {
    const metrics = {
      totalRequests: 0,
      successfulRequests: 0,
      failedRequests: 0,
      averageResponseTime: 0
    };
    
    const requestTimers = new Map();
    
    // Track request start
    apisto.addRequestInterceptor(async (options, url) => {
      requestTimers.set(url, performance.now());
      metrics.totalRequests++;
      return options;
    });
    
    // Track response success and timing
    apisto.addResponseInterceptor(async (response, options) => {
      const startTime = requestTimers.get(options.url);
      if (startTime) {
        const duration = performance.now() - startTime;
        requestTimers.delete(options.url);
        
        metrics.successfulRequests++;
        metrics.averageResponseTime = 
          (metrics.averageResponseTime * (metrics.successfulRequests - 1) + duration) / 
          metrics.successfulRequests;
        
        // Send to analytics service
        sendAnalytics('request_success', {
          url: options.url,
          method: options.method,
          duration,
          status: response.status
        });
      }
      return response;
    });
    
    // Track errors
    apisto.addErrorInterceptor(async (error, options) => {
      metrics.failedRequests++;
      
      sendAnalytics('request_error', {
        url: options.url,
        method: options.method,
        error: error.message,
        status: error.status
      });
      
      return error;
    });
    
    // Expose metrics
    apisto.getMetrics = function() {
      return { ...metrics };
    };
    
    async function sendAnalytics(event, data) {
      // Send to your analytics service
      try {
        await fetch('/api/analytics', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ event, data, timestamp: new Date().toISOString() })
        });
      } catch (error) {
        console.warn('Analytics send failed:', error);
      }
    }
  }
};
            

Plugin Configuration

Configure plugins with options and handle plugin dependencies.


// Plugin with configuration options
const ConfigurablePlugin = {
  name: 'configurable-plugin',
  
  install(apisto, options = {}) {
    const {
      enabled = true,
      logLevel = 'info',
      maxRetries = 3,
      customHeader = null
    } = options;
    
    if (!enabled) {
      console.log('ConfigurablePlugin is disabled');
      return;
    }
    
    // Store plugin instance for later access
    const pluginInstance = {
      options,
      log(message, level = 'info') {
        if (level === logLevel || logLevel === 'debug') {
          console.log(`[${this.name}] ${message}`);
        }
      }
    };
    
    apisto.plugins[this.name] = pluginInstance;
    
    // Add custom header if specified
    if (customHeader) {
      apisto.addRequestInterceptor(async (options) => {
        options.headers[customHeader.name] = customHeader.value;
        return options;
      });
    }
    
    // Add retry logic
    apisto.addErrorInterceptor(async (error, requestOptions) => {
      if (error.status >= 500 && maxRetries > 0) {
        const retryCount = requestOptions._retryCount || 0;
        
        if (retryCount < maxRetries) {
          pluginInstance.log(`Retrying request (${retryCount + 1}/${maxRetries})`);
          
          requestOptions._retryCount = retryCount + 1;
          await new Promise(resolve => 
            setTimeout(resolve, Math.pow(2, retryCount) * 1000)
          );
          
          return apisto.request(requestOptions.url, requestOptions);
        }
      }
      return error;
    });
    
    pluginInstance.log('Plugin installed successfully');
  }
};

// Usage with configuration
const api = new Apisto({
  baseURL: 'https://api.example.com'
});

api.use(ConfigurablePlugin, {
  enabled: true,
  logLevel: 'debug',
  maxRetries: 5,
  customHeader: {
    name: 'X-Plugin-Enabled',
    value: 'true'
  }
});
        

Plugin Lifecycle

Understand when and how plugins are executed during Apisto's lifecycle.


const LifecyclePlugin = {
  name: 'lifecycle-plugin',
  
  // Called when plugin is registered
  install(apisto) {
    console.log('📦 Plugin installed');
    
    // Store reference to apisto instance
    this.apisto = apisto;
    
    // Request phase
    apisto.addRequestInterceptor(async (options, url) => {
      console.log('🚀 Request starting:', url);
      options._startTime = Date.now();
      return options;
    }, 'request-start');
    
    // Response phase
    apisto.addResponseInterceptor(async (response, options) => {
      const duration = Date.now() - options._startTime;
      console.log(`✅ Request completed in ${duration}ms`);
      return response;
    }, 'response-success');
    
    // Error phase
    apisto.addErrorInterceptor(async (error, options) => {
      console.log('❌ Request failed:', error.message);
      return error;
    }, 'response-error');
  },
  
  // Optional: Called when plugin is unregistered
  uninstall() {
    console.log('🗑️ Plugin uninstalled');
    // Clean up any event listeners, timers, etc.
    if (this.apisto) {
      // Remove interceptors, etc.
    }
  },
  
  // Optional: Called before request is sent
  beforeRequest(options) {
    console.log('🔧 Before request:', options);
    return options;
  },
  
  // Optional: Called after response is received
  afterResponse(response, options) {
    console.log('🔧 After response:', response.status);
    return response;
  }
};

// Plugin with multiple interceptors at different phases
const MultiPhasePlugin = {
  name: 'multi-phase-plugin',
  
  install(apisto) {
    // Early request phase - modify headers
    apisto.addRequestInterceptor(async (options) => {
      options.headers['X-Phase'] = 'early';
      return options;
    }, 'early');
    
    // Late request phase - add timestamps
    apisto.addRequestInterceptor(async (options) => {
      options.headers['X-Timestamp'] = Date.now();
      return options;
    }, 'late');
    
    // Early response phase - log response
    apisto.addResponseInterceptor(async (response) => {
      console.log('Early response processing');
      return response;
    }, 'early');
    
    // Late response phase - transform data
    apisto.addResponseInterceptor(async (response) => {
      const data = await response.clone().json();
      data.processedAt = new Date().toISOString();
      return new Response(JSON.stringify(data), response);
    }, 'late');
  }
};
        

Community Plugins

Discover popular community plugins and how to use them.

🔄 apisto-retry

Advanced retry logic with exponential backoff and custom retry conditions.


import { RetryPlugin } from 'apisto-retry';

api.use(RetryPlugin, {
  maxRetries: 3,
  backoff: 'exponential'
});
              

📊 apisto-metrics

Comprehensive metrics collection and performance monitoring.


import { MetricsPlugin } from 'apisto-metrics';

api.use(MetricsPlugin, {
  sendTo: 'https://metrics.example.com'
});
              

🔐 apisto-auth

Complete authentication flow with token refresh and storage.


import { AuthPlugin } from 'apisto-auth';

api.use(AuthPlugin, {
  storage: 'localStorage',
  refreshEndpoint: '/auth/refresh'
});
              

💾 apisto-offline

Offline support with request queuing and automatic sync when online.


import { OfflinePlugin } from 'apisto-offline';

api.use(OfflinePlugin, {
  queueSize: 100,
  syncOnReconnect: true
});
              

Plugin Best Practices

  • Keep plugins focused: Each plugin should handle one specific concern
  • Handle errors gracefully: Don't let plugin errors break the entire request flow
  • Use proper naming: Prefix plugin names to avoid conflicts
  • Document your plugins: Include usage examples and configuration options
  • Test thoroughly: Ensure plugins work correctly in different scenarios
  • Consider performance: Avoid heavy operations in interceptors

Mastered Apisto?

You've completed the Apisto tutorial! Start building amazing applications with confidence.