This guide shows you how to integrate functionality between Backstage plugins using both frontend (ApiRef) and backend (ServiceRef) approaches following the latest Backstage patterns.
- When to Use ServiceRef vs ApiRef
- Frontend Plugin Integration
- Backend Plugin Integration
- Best Practices
- Common Issues
Source documentation:
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
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
// 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 }),
}),
],
});
// 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>;
};
// 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');
},
});
},
});
// 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
}
}
// 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();
// 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();
},
});
},
});
-
API/Service Design
- Keep interfaces simple and focused
- Use clear naming that indicates purpose
- Document public APIs
-
Dependency Management
- Declare all dependencies explicitly
- Use specific versions in package.json
- Follow the principle of least privilege
-
Backend Services
- Default to plugin scope unless root is needed
- Use
impl
for shared services (database, cache) - Use
factory
for plugin-specific services
-
Service Not Found
- Ensure service is registered before use
- Check service ID matches
- Verify plugin dependencies
-
API Initialization
- Register APIs before plugin use
- Check API factory dependencies
- Verify API implementation