Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save brandonbryant12/28cb7ad1cf5372e71478a8fb70911eb2 to your computer and use it in GitHub Desktop.
Save brandonbryant12/28cb7ad1cf5372e71478a8fb70911eb2 to your computer and use it in GitHub Desktop.

Backstage Cross Plugin Integration Guide

Overview

This guide shows you how to integrate functionality between Backstage plugins using both frontend (ApiRef) and backend (ServiceRef) approaches following the latest Backstage patterns.

Source documentation:

When to Use ServiceRef vs ApiRef

ServiceRef

Use ServiceRef when:

  • You need to share backend functionality
  • You're working with databases or external services
  • You need to process data before serving it to the frontend
  • You want to share common backend utilities

Example scenarios:

// Good ServiceRef use cases - these are server-side services that handle sensitive operations
- Database connections      // For centralized database access
- Authentication services  // For handling user authentication
- Cache services          // For shared caching logic
- External API integrations // For third-party service integration
- File processing services // For handling file operations
- Scheduled tasks         // For background processing
- Messaging queues        // For async communication

ApiRef

Use ApiRef when:

  • You need to share frontend functionality
  • You want to provide a client-side API
  • You need to manage UI state across plugins
  • You want to share frontend utilities

Example scenarios:

// Good ApiRef use cases - these are browser-based services for UI functionality
- HTTP clients           // For making API requests
- State management       // For sharing UI state
- UI utilities          // For common UI operations
- Frontend authentication // For managing auth state
- Theme management      // For consistent styling
- Feature flags         // For feature toggling
- Analytics tracking    // For user behavior tracking

Frontend Plugin Integration

1. Create Plugin and API

// plugins/your-plugin/src/plugin.ts
import { 
  createPlugin, 
  createApiRef,
  createApiFactory
} from '@backstage/core-plugin-api';
import { discoveryApiRef } from '@backstage/core-plugin-api';

// Define API contract
export interface YourPluginApi {
  getData(): Promise<any>;
}

// Create API reference
export const yourPluginApiRef = createApiRef<YourPluginApi>({
  id: 'plugin.your-plugin.api',
});

// Implement API
class YourPluginClient implements YourPluginApi {
  private readonly discoveryApi: DiscoveryApi;

  constructor(options: { discoveryApi: DiscoveryApi }) {
    this.discoveryApi = options.discoveryApi;
  }

  async getData(): Promise<any> {
    const baseUrl = await this.discoveryApi.getBaseUrl('your-plugin');
    const response = await fetch(`${baseUrl}/data`);
    return response.json();
  }
}

// Create and export plugin with API factory
export const yourPlugin = createPlugin({
  id: 'your-plugin',
  apis: [
    createApiFactory({
      api: yourPluginApiRef,
      deps: { discoveryApi: discoveryApiRef },
      factory: ({ discoveryApi }) => new YourPluginClient({ discoveryApi }),
    }),
  ],
});

2. Use API in Components

// plugins/your-plugin/src/components/YourComponent.tsx
import { useApi } from '@backstage/core-plugin-api';
import { yourPluginApiRef } from '../plugin';

export const YourComponent = () => {
  const api = useApi(yourPluginApiRef);
  const [data, setData] = useState<any>();

  useEffect(() => {
    const fetchData = async () => {
      const result = await api.getData();
      setData(result);
    };
    fetchData();
  }, [api]);

  return <div>{/* Render data */}</div>;
};

Backend Plugin Integration

1. Define Plugin and Service

// plugins/your-backend-plugin/src/plugin.ts
import { 
  createBackendPlugin,
  createServiceRef,
  coreServices,
} from '@backstage/backend-plugin-api';

// Define service interface
export interface YourPluginService {
  processData(): Promise<void>;
}

// Create service reference
export const yourPluginServiceRef = createServiceRef<YourPluginService>({
  id: 'your-plugin.service',
  scope: 'plugin', // Use 'plugin' by default, 'root' for shared services
});

// Create plugin with environment registration
export const yourBackendPlugin = createBackendPlugin({
  pluginId: 'your-plugin',
  register(env) {
    env.registerInit({
      deps: {
        logger: coreServices.logger,
        httpRouter: coreServices.httpRouter,
      },
      async init({ logger, httpRouter }) {
        const router = await createRouter({
          logger,
        });
        
        httpRouter.use(router);
        logger.info('Your plugin initialized');
      },
    });
  },
});

2. Implement Service

// plugins/your-backend-plugin/src/service/YourPluginService.ts
import { Logger } from '@backstage/backend-plugin-api';
import { YourPluginService } from '../plugin';

export class DefaultYourPluginService implements YourPluginService {
  private readonly logger: Logger;

  constructor(options: { logger: Logger }) {
    this.logger = options.logger;
  }

  async processData(): Promise<void> {
    this.logger.info('Processing data');
    // Implementation
  }
}

3. Register Plugin and Service with Main Backend

// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-app-api';
import { yourBackendPlugin, yourPluginServiceRef } from '@internal/plugin-your-backend';
import { DefaultYourPluginService } from '@internal/plugin-your-backend/src/service/YourPluginService';

async function main() {
  const backend = createBackend();

  // Add your plugin
  backend.add(yourBackendPlugin);

  // Example 1: Using 'impl' - Single shared instance
  // Use this when:
  // - You need one instance shared across all plugins
  // - Managing shared resources (database connections, caches)
  // - Service maintains global state
  // - Service needs to be a singleton
  // Common examples: Database connections, authentication services, rate limiters
  backend.register({
    ref: yourPluginServiceRef,
    impl: new DefaultYourPluginService({
      catalogClient: backend.getService(catalogServiceRef).getClient(),
      logger: backend.getService(coreServices.logger),
      // All plugins will share this same instance
      // State and resources are shared
      cache: globalCacheInstance,
      maxConnections: 10,
    }),
  });

  // Example 2: Using 'factory' - New instance per plugin
  // Use this when:
  // - Each plugin needs its own isolated instance
  // - Service needs plugin-specific configuration
  // - Plugins need isolated state
  // - You want to namespace resources per plugin
  // Common examples: Plugin-specific storage, feature flags, API clients
  backend.register({
    ref: yourPluginServiceRef,
    factory: ({ plugin, config, logger }) => {
      return new DefaultYourPluginService({
        catalogClient: backend.getService(catalogServiceRef).getClient(),
        logger: logger.child({ plugin: plugin.getId() }),
        // Each plugin gets its own isolated instance
        // State and resources are isolated per plugin
        cache: new Cache({ namespace: plugin.getId() }),
        pluginConfig: config.getConfig(`plugins.${plugin.getId()}`),
      });
    },
  });

  await backend.start();
}

main();

4. Use Service in Another Plugin

// plugins/another-backend-plugin/src/plugin.ts
import { yourPluginServiceRef } from '@internal/plugin-your-backend';

export const anotherBackendPlugin = createBackendPlugin({
  pluginId: 'another-plugin',
  register(env) {
    env.registerInit({
      deps: { 
        yourService: yourPluginServiceRef,
        logger: coreServices.logger,
      },
      async init({ yourService, logger }) {
        await yourService.processData();
      },
    });
  },
});

Best Practices

  1. API/Service Design

    • Keep interfaces simple and focused
    • Use clear naming that indicates purpose
    • Document public APIs
  2. Dependency Management

    • Declare all dependencies explicitly
    • Use specific versions in package.json
    • Follow the principle of least privilege
  3. Backend Services

    • Default to plugin scope unless root is needed
    • Use impl for shared services (database, cache)
    • Use factory for plugin-specific services

Common Issues

  1. Service Not Found

    • Ensure service is registered before use
    • Check service ID matches
    • Verify plugin dependencies
  2. API Initialization

    • Register APIs before plugin use
    • Check API factory dependencies
    • Verify API implementation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment