Utility APIs
Version: Backstage v1.42.0+ Status: Complete Reference Last Updated: 2025-10-27
Overview
Utility APIs are reusable services that provide functionality to components and extensions throughout your Backstage app. They enable dependency injection, separation of concerns, and testability.
This guide covers creating, registering, and consuming Utility APIs in the New Frontend System.
Table of Contents
- What Are Utility APIs?
- Standard Utility APIs
- Creating API Refs
- Implementing APIs
- Registering APIs
- Consuming APIs
- API Dependencies
- Testing with APIs
- Best Practices
What Are Utility APIs?
Utility APIs are services that provide common functionality across your Backstage app. They follow a dependency injection pattern where:
- API Reference - Defines the contract (interface)
- API Implementation - Provides the functionality
- API Registration - Makes the API available to the app
- API Consumption - Components use the API via hooks
Why Utility APIs?
Abstraction: Hide implementation details behind interfaces
Dependency Injection: Components receive dependencies rather than creating them
Testability: Easy to mock APIs in tests
Reusability: Share logic across plugins and components
Type Safety: TypeScript interfaces ensure correct usage
Standard Utility APIs
Backstage provides many standard Utility APIs out of the box.
Core APIs
import {
configApiRef,
discoveryApiRef,
identityApiRef,
fetchApiRef,
errorApiRef,
storageApiRef,
analyticsApiRef,
alertApiRef,
} from '@backstage/core-plugin-api';
configApiRef
Access app configuration from app-config.yaml.
interface ConfigApi {
get<T>(key?: string): T | undefined;
getOptional<T>(key?: string): T | undefined;
getConfig(key: string): Config;
getOptionalConfig(key: string): Config | undefined;
has(key: string): boolean;
keys(): string[];
}
Usage:
const configApi = useApi(configApiRef);
const appTitle = configApi.getString('app.title');
const backendUrl = configApi.getString('backend.baseUrl');
discoveryApiRef
Discover backend plugin URLs.
interface DiscoveryApi {
getBaseUrl(pluginId: string): Promise<string>;
}
Usage:
const discoveryApi = useApi(discoveryApiRef);
const catalogUrl = await discoveryApi.getBaseUrl('catalog');
// Returns: http://localhost:7007/api/catalog
identityApiRef
Get current user identity and credentials.
interface IdentityApi {
getUserId(): string;
getIdToken(): Promise<string | undefined>;
getProfile(): ProfileInfo;
getProfileInfo(): Promise<ProfileInfo>;
getBackstageIdentity(): Promise<BackstageIdentity>;
getCredentials(): Promise<{ token?: string }>;
signOut(): Promise<void>;
}
Usage:
const identityApi = useApi(identityApiRef);
const profile = await identityApi.getProfileInfo();
const credentials = await identityApi.getCredentials();
fetchApiRef
Make HTTP requests with automatic auth header injection.
interface FetchApi {
fetch(input: string | Request, init?: RequestInit): Promise<Response>;
}
Usage:
const fetchApi = useApi(fetchApiRef);
const response = await fetchApi.fetch('/api/catalog/entities');
const data = await response.json();
errorApiRef
Post errors to centralized error handling.
interface ErrorApi {
post(error: Error, context?: ErrorContext): void;
error$(options?: ErrorApiErrorOptions): Observable<{ error: Error; context?: ErrorContext }>;
}
Usage:
const errorApi = useApi(errorApiRef);
try {
await riskyOperation();
} catch (error) {
errorApi.post(error);
}
storageApiRef
Browser storage with namespacing and observability.
interface StorageApi {
set<T>(key: string, data: T): void;
get<T>(key: string): T | undefined;
remove(key: string): void;
observe$<T>(key: string): Observable<{ key: string; newValue: T | undefined }>;
}
Usage:
const storageApi = useApi(storageApiRef);
storageApi.set('theme', 'dark');
const theme = storageApi.get<string>('theme');
Auth APIs
import {
githubAuthApiRef,
googleAuthApiRef,
oktaAuthApiRef,
microsoftAuthApiRef,
} from '@backstage/core-plugin-api';
All auth APIs implement common interfaces:
interface OAuthApi {
getAccessToken(scope?: string | string[], options?: AuthRequestOptions): Promise<string>;
}
interface OpenIdConnectApi {
getIdToken(options?: AuthRequestOptions): Promise<string>;
}
interface ProfileInfoApi {
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo>;
}
interface BackstageIdentityApi {
getBackstageIdentity(options?: AuthRequestOptions): Promise<BackstageIdentity>;
}
interface SessionApi {
signIn(): Promise<void>;
signOut(): Promise<void>;
sessionState$(): Observable<SessionState>;
}
Usage:
const githubAuth = useApi(githubAuthApiRef);
await githubAuth.signIn();
const token = await githubAuth.getAccessToken();
const profile = await githubAuth.getProfile();
Plugin-Specific APIs
Many plugins provide their own APIs:
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import { techdocsStorageApiRef } from '@backstage/plugin-techdocs-react';
Creating API Refs
An API ref defines the contract for your API.
Basic API Ref
import { createApiRef } from '@backstage/core-plugin-api';
export interface WeatherApi {
getCurrentWeather(location: string): Promise<WeatherData>;
getForecast(location: string, days: number): Promise<ForecastData>;
}
export const weatherApiRef = createApiRef<WeatherApi>({
id: 'plugin.weather.api',
});
API Ref with Multiple Interfaces
Some APIs implement multiple interfaces:
export interface CustomAuthApi
extends OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionApi {
// Additional custom methods
refreshToken(): Promise<void>;
}
export const customAuthApiRef = createApiRef<CustomAuthApi>({
id: 'plugin.custom-auth.api',
});
Generic API Ref
export interface CacheApi<T> {
get(key: string): Promise<T | undefined>;
set(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
}
export const cacheApiRef = createApiRef<CacheApi<any>>({
id: 'plugin.cache.api',
});
Implementing APIs
Create a class that implements your API interface.
Basic Implementation
import { WeatherApi, weatherApiRef } from './api';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
export class WeatherApiClient implements WeatherApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
}) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
}
async getCurrentWeather(location: string): Promise<WeatherData> {
const baseUrl = await this.discoveryApi.getBaseUrl('weather');
const response = await this.fetchApi.fetch(
`${baseUrl}/current?location=${encodeURIComponent(location)}`
);
if (!response.ok) {
throw new Error(`Failed to fetch weather: ${response.statusText}`);
}
return await response.json();
}
async getForecast(location: string, days: number): Promise<ForecastData> {
const baseUrl = await this.discoveryApi.getBaseUrl('weather');
const response = await this.fetchApi.fetch(
`${baseUrl}/forecast?location=${encodeURIComponent(location)}&days=${days}`
);
if (!response.ok) {
throw new Error(`Failed to fetch forecast: ${response.statusText}`);
}
return await response.json();
}
}
Implementation with Configuration
export class WeatherApiClient implements WeatherApi {
private readonly apiKey: string;
private readonly fetchApi: FetchApi;
constructor(options: {
configApi: ConfigApi;
fetchApi: FetchApi;
}) {
this.apiKey = options.configApi.getString('weather.apiKey');
this.fetchApi = options.fetchApi;
}
async getCurrentWeather(location: string): Promise<WeatherData> {
const response = await this.fetchApi.fetch(
`https://api.weather.com/current?location=${location}&apiKey=${this.apiKey}`
);
return await response.json();
}
}
Implementation with Caching
export class CachedWeatherApiClient implements WeatherApi {
private readonly delegate: WeatherApi;
private readonly cache = new Map<string, { data: any; expires: number }>();
constructor(delegate: WeatherApi) {
this.delegate = delegate;
}
async getCurrentWeather(location: string): Promise<WeatherData> {
const cacheKey = `current:${location}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
const data = await this.delegate.getCurrentWeather(location);
this.cache.set(cacheKey, {
data,
expires: Date.now() + 5 * 60 * 1000, // 5 minutes
});
return data;
}
async getForecast(location: string, days: number): Promise<ForecastData> {
return this.delegate.getForecast(location, days);
}
}
Registering APIs
APIs must be registered to make them available to components.
Using ApiBlueprint (Recommended)
import { ApiBlueprint } from '@backstage/frontend-plugin-api';
import {
discoveryApiRef,
fetchApiRef
} from '@backstage/core-plugin-api';
import { weatherApiRef, WeatherApiClient } from './api';
export const weatherApi = ApiBlueprint.make({
name: 'weather',
params: {
api: weatherApiRef,
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, fetchApi }) => {
return new WeatherApiClient({ discoveryApi, fetchApi });
},
},
});
Registering in Frontend Module
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { weatherApi } from './apis';
export const weatherModule = createFrontendModule({
pluginId: 'weather',
extensions: [weatherApi],
});
Registering in App
import { createApp } from '@backstage/frontend-defaults';
import { weatherModule } from '@backstage-community/plugin-weather';
const app = createApp({
features: [
weatherModule,
// other features...
],
});
With Configuration
export const weatherApi = ApiBlueprint.make({
name: 'weather',
params: {
api: weatherApiRef,
deps: {
configApi: configApiRef,
fetchApi: fetchApiRef,
},
factory: ({ configApi, fetchApi }) => {
const enabled = configApi.getOptionalBoolean('weather.enabled') ?? true;
if (!enabled) {
return new MockWeatherApiClient();
}
return new WeatherApiClient({ configApi, fetchApi });
},
},
});
Consuming APIs
Components consume APIs using the useApi hook.
Basic Usage
import React from 'react';
import { useApi } from '@backstage/core-plugin-api';
import { weatherApiRef } from '../api';
export const WeatherWidget = () => {
const weatherApi = useApi(weatherApiRef);
const [weather, setWeather] = React.useState<WeatherData | null>(null);
React.useEffect(() => {
weatherApi.getCurrentWeather('New York').then(setWeather);
}, [weatherApi]);
if (!weather) {
return <div>Loading...</div>;
}
return (
<div>
<h2>Current Weather</h2>
<p>Temperature: {weather.temperature}°F</p>
<p>Conditions: {weather.conditions}</p>
</div>
);
};
With Error Handling
export const WeatherWidget = () => {
const weatherApi = useApi(weatherApiRef);
const errorApi = useApi(errorApiRef);
const [weather, setWeather] = React.useState<WeatherData | null>(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
let mounted = true;
weatherApi.getCurrentWeather('New York')
.then(data => {
if (mounted) {
setWeather(data);
setLoading(false);
}
})
.catch(error => {
errorApi.post(error);
setLoading(false);
});
return () => {
mounted = false;
};
}, [weatherApi, errorApi]);
if (loading) {
return <div>Loading...</div>;
}
if (!weather) {
return <div>Failed to load weather</div>;
}
return <div>{/* ... */}</div>;
};
Using Multiple APIs
export const UserDashboard = () => {
const identityApi = useApi(identityApiRef);
const catalogApi = useApi(catalogApiRef);
const weatherApi = useApi(weatherApiRef);
const [profile, setProfile] = React.useState<ProfileInfo | null>(null);
const [entities, setEntities] = React.useState<Entity[]>([]);
const [weather, setWeather] = React.useState<WeatherData | null>(null);
React.useEffect(() => {
Promise.all([
identityApi.getProfileInfo(),
catalogApi.getEntities({ filter: { kind: 'Component' } }),
weatherApi.getCurrentWeather('New York'),
]).then(([profileData, entitiesData, weatherData]) => {
setProfile(profileData);
setEntities(entitiesData.items);
setWeather(weatherData);
});
}, [identityApi, catalogApi, weatherApi]);
return <div>{/* ... */}</div>;
};
Optional API Usage
import { useApi, useApiOptional } from '@backstage/core-plugin-api';
import { analyticsApiRef } from '@backstage/core-plugin-api';
export const TrackedButton = () => {
// Optional: returns undefined if API not registered
const analyticsApi = useApiOptional(analyticsApiRef);
const handleClick = () => {
// Track only if analytics is available
analyticsApi?.captureEvent('button_clicked', { button: 'submit' });
// Do actual work
submitForm();
};
return <button onClick={handleClick}>Submit</button>;
};
API Dependencies
APIs can depend on other APIs.
Simple Dependencies
export const notificationApi = ApiBlueprint.make({
name: 'notification',
params: {
api: notificationApiRef,
deps: {
alertApi: alertApiRef,
storageApi: storageApiRef,
},
factory: ({ alertApi, storageApi }) => {
return new NotificationApiClient({ alertApi, storageApi });
},
},
});
Complex Dependencies
export const complexApi = ApiBlueprint.make({
name: 'complex',
params: {
api: complexApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
identityApi: identityApiRef,
storageApi: storageApiRef,
errorApi: errorApiRef,
},
factory: (deps) => {
return new ComplexApiClient(deps);
},
},
});
Conditional Dependencies
export const analyticsApi = ApiBlueprint.make({
name: 'analytics',
params: {
api: analyticsApiRef,
deps: {
configApi: configApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, identityApi }) => {
const provider = configApi.getOptionalString('analytics.provider');
switch (provider) {
case 'google':
return new GoogleAnalyticsClient({ configApi, identityApi });
case 'segment':
return new SegmentAnalyticsClient({ configApi, identityApi });
default:
return new NoOpAnalyticsClient();
}
},
},
});
Testing with APIs
Utility APIs make components highly testable.
Mocking APIs in Tests
import { TestApiProvider } from '@backstage/test-utils';
import { weatherApiRef } from '../api';
const mockWeatherApi: Partial<WeatherApi> = {
getCurrentWeather: jest.fn().mockResolvedValue({
temperature: 72,
conditions: 'Sunny',
}),
};
describe('WeatherWidget', () => {
it('displays weather data', async () => {
const { getByText } = render(
<TestApiProvider apis={[[weatherApiRef, mockWeatherApi]]}>
<WeatherWidget />
</TestApiProvider>
);
await waitFor(() => {
expect(getByText(/Temperature: 72/)).toBeInTheDocument();
expect(getByText(/Conditions: Sunny/)).toBeInTheDocument();
});
});
});
Testing API Implementations
describe('WeatherApiClient', () => {
let weatherApi: WeatherApiClient;
let mockDiscoveryApi: jest.Mocked<DiscoveryApi>;
let mockFetchApi: jest.Mocked<FetchApi>;
beforeEach(() => {
mockDiscoveryApi = {
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007/api/weather'),
};
mockFetchApi = {
fetch: jest.fn(),
};
weatherApi = new WeatherApiClient({
discoveryApi: mockDiscoveryApi,
fetchApi: mockFetchApi,
});
});
it('fetches current weather', async () => {
mockFetchApi.fetch.mockResolvedValue({
ok: true,
json: async () => ({ temperature: 72, conditions: 'Sunny' }),
} as Response);
const result = await weatherApi.getCurrentWeather('New York');
expect(result).toEqual({ temperature: 72, conditions: 'Sunny' });
expect(mockFetchApi.fetch).toHaveBeenCalledWith(
'http://localhost:7007/api/weather/current?location=New%20York'
);
});
it('handles errors', async () => {
mockFetchApi.fetch.mockResolvedValue({
ok: false,
statusText: 'Not Found',
} as Response);
await expect(weatherApi.getCurrentWeather('InvalidLocation'))
.rejects.toThrow('Failed to fetch weather: Not Found');
});
});
Best Practices
1. Define Clear Interfaces
Good:
export interface WeatherApi {
/** Get current weather for a location */
getCurrentWeather(location: string): Promise<WeatherData>;
/** Get weather forecast for the next N days */
getForecast(location: string, days: number): Promise<ForecastData>;
}
Avoid:
export interface WeatherApi {
get(type: string, ...args: any[]): Promise<any>;
}
2. Use Descriptive API Ref IDs
Good:
createApiRef<WeatherApi>({
id: 'plugin.weather.api', // Namespaced, descriptive
});
Avoid:
createApiRef<WeatherApi>({
id: 'weather', // Too generic, collision risk
});
3. Handle Errors Gracefully
Good:
async getCurrentWeather(location: string): Promise<WeatherData> {
const response = await this.fetchApi.fetch(url);
if (!response.ok) {
throw new Error(
`Weather API error: ${response.status} ${response.statusText}`
);
}
return await response.json();
}
4. Cache When Appropriate
export class CachedApiClient implements MyApi {
private cache = new Map<string, CacheEntry>();
async getData(key: string): Promise<Data> {
const cached = this.cache.get(key);
if (cached && !this.isExpired(cached)) {
return cached.data;
}
const fresh = await this.fetchFreshData(key);
this.cache.set(key, { data: fresh, timestamp: Date.now() });
return fresh;
}
}
5. Make APIs Testable
- Accept dependencies via constructor
- Use interfaces, not concrete classes
- Avoid global state
- Return promises for async operations
6. Document Your APIs
/**
* API for interacting with the weather backend.
*
* @public
*/
export interface WeatherApi {
/**
* Get current weather conditions for a location.
*
* @param location - City name or coordinates
* @returns Current weather data
* @throws {Error} If location is invalid or API is unavailable
*/
getCurrentWeather(location: string): Promise<WeatherData>;
}
7. Version Your API Refs
When making breaking changes, create new API refs:
// Old version (deprecated)
export const weatherApiRef = createApiRef<WeatherApi>({
id: 'plugin.weather.api',
});
// New version
export const weatherApiV2Ref = createApiRef<WeatherApiV2>({
id: 'plugin.weather.api.v2',
});
8. Use TypeScript Strictly
// Define strict types
export interface WeatherData {
temperature: number;
conditions: string;
humidity: number;
windSpeed: number;
}
// Not 'any'
export interface WeatherApi {
getCurrentWeather(location: string): Promise<WeatherData>; // ✓
// getCurrentWeather(location: string): Promise<any>; // ✗
}
Common Patterns
Pattern 1: Backend API Client
export class BackendApiClient implements MyApi {
constructor(
private readonly discoveryApi: DiscoveryApi,
private readonly fetchApi: FetchApi,
) {}
async getData(id: string): Promise<Data> {
const baseUrl = await this.discoveryApi.getBaseUrl('my-plugin');
const response = await this.fetchApi.fetch(`${baseUrl}/data/${id}`);
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
return await response.json();
}
}
Pattern 2: Configuration-Based API
export class ConfigurableApiClient implements MyApi {
private readonly endpoint: string;
private readonly timeout: number;
constructor(configApi: ConfigApi, fetchApi: FetchApi) {
this.endpoint = configApi.getString('myPlugin.endpoint');
this.timeout = configApi.getOptionalNumber('myPlugin.timeout') ?? 30000;
// ...
}
}
Pattern 3: Delegating API
export class LoggingApiDecorator implements MyApi {
constructor(
private readonly delegate: MyApi,
private readonly logger: Logger,
) {}
async getData(id: string): Promise<Data> {
this.logger.info(`Fetching data for ${id}`);
const result = await this.delegate.getData(id);
this.logger.info(`Fetched data for ${id}`);
return result;
}
}
Pattern 4: Observable API
export interface RealtimeApi {
subscribe(topic: string): Observable<Message>;
}
export class WebSocketRealtimeApi implements RealtimeApi {
subscribe(topic: string): Observable<Message> {
return new Observable(subscriber => {
const ws = new WebSocket(`wss://api.example.com/${topic}`);
ws.onmessage = (event) => {
subscriber.next(JSON.parse(event.data));
};
ws.onerror = (error) => {
subscriber.error(error);
};
return () => {
ws.close();
};
});
}
}
Summary
Key Takeaways:
- Utility APIs provide reusable services - Configuration, HTTP, identity, storage, etc.
- Use ApiBlueprint - Simplifies registration and dependency injection
- Define clear interfaces - TypeScript interfaces ensure correct usage
- Inject dependencies - Makes APIs testable and flexible
- useApi hook - Components consume APIs via hooks
- Standard APIs available - config, discovery, identity, fetch, error, storage, auth
- Test with mocks - TestApiProvider makes components highly testable
Next Steps:
Navigation: