Skip to main content

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

  1. What Are Utility APIs?
  2. Standard Utility APIs
  3. Creating API Refs
  4. Implementing APIs
  5. Registering APIs
  6. Consuming APIs
  7. API Dependencies
  8. Testing with APIs
  9. 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:

  1. API Reference - Defines the contract (interface)
  2. API Implementation - Provides the functionality
  3. API Registration - Makes the API available to the app
  4. 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.

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:

  1. Utility APIs provide reusable services - Configuration, HTTP, identity, storage, etc.
  2. Use ApiBlueprint - Simplifies registration and dependency injection
  3. Define clear interfaces - TypeScript interfaces ensure correct usage
  4. Inject dependencies - Makes APIs testable and flexible
  5. useApi hook - Components consume APIs via hooks
  6. Standard APIs available - config, discovery, identity, fetch, error, storage, auth
  7. Test with mocks - TestApiProvider makes components highly testable

Next Steps:


Navigation: