
backstage-frontend-plugin
Build Backstage frontend plugins with the new Frontend System: createFrontendPlugin, blueprints, rou
Backstage Frontend Plugin (New Frontend System)
About This Skill
This skill provides specialized knowledge and workflows for building Backstage frontend plugins using the New Frontend System (NFS). It guides the development of UI features including pages, navigation items, entity cards/content, and shared Utility APIs.
What This Skill Provides
- Specialized workflows - Step-by-step procedures for creating and configuring frontend plugins
- Best practices - Production-ready patterns for lazy loading, error handling, permissions, and testing
- Golden path templates - Copy/paste code snippets for common plugin patterns
- Domain expertise - Backstage-specific knowledge about blueprints, routes, and the plugin system
When to Use This Skill
Use this skill when creating UI features for Backstage: pages, navigation items, entity cards/content, or shared Utility APIs.
Development Workflow
Phase 1: Planning and Research
1.1 Understand the Requirements
Before building a frontend plugin, clearly understand:
- What UI features are needed (pages, navigation, entity content, cards)
- What data the plugin will display or interact with
- Whether Utility APIs are needed for shared logic
- Integration points with other plugins (catalog, permissions, etc.)
1.2 Load Reference Documentation
Load reference files as needed based on the plugin requirements:
For Extension Development:
- 📋 Extension Blueprints Reference - Comprehensive guide to PageBlueprint, SubPageBlueprint, EntityContentBlueprint, ApiBlueprint, and app-level blueprints
For Utility API Development:
- 🔌 Utility APIs Reference - Creating and using Utility APIs for shared logic
For Testing:
- ✅ Testing Reference - Comprehensive testing guide for components, extensions, and APIs
Phase 2: Implementation
Follow the Golden Path workflow below for implementation, referring to reference files as needed.
Phase 3: Testing
After implementing the plugin:
- Load the ✅ Testing Reference
- Write comprehensive tests for:
- React components using
renderInTestAppwith theapisoption - Extensions using
createExtensionTester(...).reactElement() - Utility APIs with mocked dependencies
- React components using
- Run tests and achieve good coverage:
yarn backstage-cli package test --coverage --watchAll=false
Phase 4: Review and Polish
Before publishing:
- Run linting and structure checks
- Ensure all extensions are listed in the plugin's
extensionsarray - Verify lazy loading via dynamic imports in every
loader - Check permission-based visibility where appropriate
- Review the Common Pitfalls section below
Quick Facts
- Create a plugin with
yarn new→ selectfrontend-plugin; it generatesplugins/<pluginId>/already wired for the New Frontend System. - The plugin instance is built with
createFrontendPluginfrom@backstage/frontend-plugin-api. Export it as the default fromsrc/index.ts. - Functionality is provided via extensions (
PageBlueprint,SubPageBlueprint,ApiBlueprint,EntityContentBlueprint, etc.). Page content is always lazy-loaded using dynamic imports. - Define route references with
createRouteRef(usually insrc/routes.ts) and use them in blueprints. - Use Utility APIs for shared logic (
createApiRef+ApiBlueprint), consumed viauseApi. - Hooks (
useApi,useRouteRef) and built-in API refs (discoveryApiRef,fetchApiRef,identityApiRef,alertApiRef,errorApiRef,storageApiRef) should be imported from@backstage/frontend-plugin-apiin NFS code — they are still re-exported from@backstage/core-plugin-apibut that is the legacy import path. - The
dev/index.tsxentrypoint usescreateDevAppfrom@backstage/frontend-dev-utilsand is started viayarn startinside the plugin directory.
App integration patterns
- New Frontend System (preferred):
- Apps using
@backstage/frontend-defaultsauto-discover a plugin's extensions when the package is installed and listed as a feature. - Just install the plugin; routes declared by
PageBlueprintmount automatically and theirtitle/icondrive the sidebar.
- Apps using
- Legacy apps (compat path):
- Only relevant if your target app is still using
@backstage/app-defaultsand manualFlatRoutes. Most new work should target NFS.
- Only relevant if your target app is still using
Production Best Practices
Extensions and Routes
- Keep all
routeRefs insrc/routes.ts - Use
createSubRouteReffor nested paths - Export only the plugin (default from
src/index.ts) - Never export extensions/components from the package root
Lazy Loading and UX
PageBlueprint automatically wraps the loader with React Suspense and an error boundary — no need to add them yourself at the extension level. Just keep your loader as a dynamic import:
loader: () => import('./components/ExamplePage').then(m => <m.ExamplePage />),
If you need an error boundary inside a component (e.g. around a risky subtree), use ErrorBoundary from @backstage/core-components.
Utility APIs
- Keep API interfaces small and stable
- Wrap external clients/fetchers
- Provide a mock implementation for tests
- Register via
ApiBlueprintand consume withuseApifrom@backstage/frontend-plugin-api - Do not fetch in render — use hooks/effects (or a helper like
useAsyncfromreact-use)
Permissions and Visibility
Hide/show entity content based on permissions or ownership to avoid broken UX for unauthorized users:
import { usePermission } from '@backstage/plugin-permission-react';
import { somePermission } from '@backstage/plugin-permission-common';
export function ExampleEntityContent() {
const { loading, allowed } = usePermission({ permission: somePermission });
if (loading) return null;
if (!allowed) return null; // or render a friendly message/banner
return <div>Secret content</div>;
}
Entity Integrations
- Prefer small, focused entity content extensions
- Use the
filterparam onEntityContentBlueprintto limit to certain entity kinds - Keep presentational components separate from data hooks
Testing
- Prefer passing
apistorenderInTestApp/createExtensionTesterover wrapping withTestApiProvider - Use
mockApis.*helpers (mockApis.identity(...), etc.) for common mocks - Verify routeRefs with
useRouteRefvia themountedRoutesoption where navigation matters
Golden Path (Copy/Paste Workflow)
1) Scaffold
# From the repository root (interactive)
yarn new
# Select: frontend-plugin
# Enter plugin id (kebab case, e.g. example)
# Non-interactive (for AI agents/automation)
yarn new --select frontend-plugin --option pluginId=example --option owner=""
This creates plugins/example/ already wired for the New Frontend System:
src/plugin.tsx—createFrontendPlugin+PageBlueprintsrc/routes.ts—createRouteRef()src/index.ts—export { examplePlugin as default } from './plugin'src/components/— example page + list componentsdev/index.tsx—createDevApp({ features: [plugin] })for standalone dev
The scaffold ships a working example todo app (src/components/TodoPage/ and src/components/TodoList/). Run the cleanup script to strip the example and leave a minimal named page component:
node scripts/cleanup-scaffolding.js plugins/example
Legacy apps only: If
yarn newdetects a legacy-app repo, it will offer thelegacy-frontend-plugintemplate instead. In that case you'll need to convert manually to NFS — see the migration guide atdocs/frontend-system/building-plugins/05-migrating.md.
2) Routes (src/routes.ts)
import { createRouteRef, createSubRouteRef } from '@backstage/frontend-plugin-api';
export const rootRouteRef = createRouteRef();
// Optional: nested paths
export const detailsRouteRef = createSubRouteRef({
parent: rootRouteRef,
path: '/details/:id',
});
3) Plugin (src/plugin.tsx)
import {
createFrontendPlugin,
PageBlueprint,
} from '@backstage/frontend-plugin-api';
import ExampleIcon from '@material-ui/icons/Extension';
import { rootRouteRef } from './routes';
// Sidebar entry is auto-inferred from the page's title + icon.
const examplePage = PageBlueprint.make({
params: {
routeRef: rootRouteRef,
path: '/example',
title: 'Example',
icon: ExampleIcon,
loader: () => import('./components/ExamplePage').then(m => <m.ExamplePage />),
},
});
export const examplePlugin = createFrontendPlugin({
pluginId: 'example',
extensions: [examplePage],
routes: { root: rootRouteRef },
});
Note:
NavItemBlueprintis deprecated and has been removed entirely on current Backstage main. Nav items are now auto-inferred from anyPageBlueprintthat has atitleandicon, or from the plugin-leveltitle/icononcreateFrontendPlugin. New code should not useNavItemBlueprint.
4) Page Component (src/components/ExamplePage.tsx)
Standard React component, no NFS-specific wrapping required:
export function ExamplePage() {
return (
<div>
<h1>Example</h1>
<p>Hello from the New Frontend System!</p>
</div>
);
}
5) Index (src/index.ts)
Only export the plugin as default:
export { examplePlugin as default } from './plugin';
6) Utility API (optional)
// src/api.ts
import { createApiRef } from '@backstage/frontend-plugin-api';
export interface ExampleApi {
getExample(): { example: string };
}
export const exampleApiRef = createApiRef<ExampleApi>({ id: 'plugin.example.api' });
export class DefaultExampleApi implements ExampleApi {
getExample() {
return { example: 'Hello World!' };
}
}
Register it with ApiBlueprint and consume via useApi:
// src/plugin.tsx
import { ApiBlueprint } from '@backstage/frontend-plugin-api';
import { exampleApiRef, DefaultExampleApi } from './api';
const exampleApi = ApiBlueprint.make({
name: 'example',
params: defineParams =>
defineParams({
api: exampleApiRef,
deps: {},
factory: () => new DefaultExampleApi(),
}),
});
export const examplePlugin = createFrontendPlugin({
pluginId: 'example',
extensions: [exampleApi, examplePage],
routes: { root: rootRouteRef },
});
Note the callback parameter is defineParams — all upstream docs and real-world plugin code use this name.
7) Entity integration (optional)
import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha';
const exampleEntityContent = EntityContentBlueprint.make({
params: {
path: 'example',
title: 'Example',
loader: () =>
import('./components/ExampleEntityContent').then(m => <m.ExampleEntityContent />),
// Optional: limit to specific entity kinds
// filter: 'kind:component,api',
// Optional: tab grouping
// group: 'overview',
},
});
8) Sub-pages with tabs (optional)
Create a tabbed parent page by combining PageBlueprint (without a loader) with several SubPageBlueprint children that attach to the parent:
import {
PageBlueprint,
SubPageBlueprint,
} from '@backstage/frontend-plugin-api';
const parentPage = PageBlueprint.make({
params: {
routeRef: rootRouteRef,
path: '/example',
title: 'Example',
// No loader — inputs.pages render as tabs
},
});
const overviewTab = SubPageBlueprint.make({
name: 'overview',
params: {
path: '',
title: 'Overview',
loader: () => import('./components/Overview').then(m => <m.Overview />),
},
});
Verify in an app
- If using the new Frontend System app (
@backstage/frontend-defaults):- Include your plugin as a feature at app creation.
- Start the repo (
yarn start) and visit the path declared by yourPageBlueprint.
- Standalone dev (inside the plugin directory):
- Run
yarn start;dev/index.tsxusescreateDevAppto render your plugin with just the features you pass in.
- Run
Testing, linting & structure checks
Run tests and lints with Backstage's CLI:
# Always pass --watchAll=false when running non-interactively (CI, AI agents);
# without it jest starts in watch mode and never exits.
yarn backstage-cli package test --watchAll=false
yarn backstage-cli package lint
yarn backstage-cli repo lint
Keep a predictable structure (API layer, hooks, components, routes.ts, plugin.tsx, index.ts).
Common Pitfalls (and Fixes)
| Problem | Solution |
|---|---|
| Extensions don't render | Ensure they're passed in the plugin's extensions array; loaders must use dynamic imports. |
| Navigation/links break | Keep routeRefs in src/routes.ts and use useRouteRef from @backstage/frontend-plugin-api to generate links. |
| Consumers can't install your plugin | Export the plugin as the default export from src/index.ts. |
| "Extension must be attached to a parent" | Use the correct blueprint (PageBlueprint attaches to app/routes; SubPageBlueprint to a parent page; EntityContentBlueprint to the entity page). |
| Sidebar item missing | Provide title + icon on PageBlueprint.params, or set plugin-level title/icon on createFrontendPlugin. |
| Import hygiene warnings | In NFS code, import useApi, useRouteRef, and the built-in API refs from @backstage/frontend-plugin-api, not @backstage/core-plugin-api. |
Reference Files
📚 Documentation Library
Load these resources as needed during development:
Extension Development
- 📋 Extension Blueprints Reference — Complete guide to extension blueprints including:
PageBlueprint/SubPageBlueprintfor pages and tab hierarchiesApiBlueprintfor Utility APIsEntityContentBlueprint/EntityCardBlueprint/EntityHeaderBlueprintetc. (catalog)PluginHeaderActionBlueprint,PluginWrapperBlueprint(alpha)- App-level blueprints in
@backstage/plugin-app-react(ThemeBlueprint,SignInPageBlueprint,IconBundleBlueprint, etc.) makeWithOverridesand custom blueprint authoring
Utility API Development
- 🔌 Utility APIs Reference — Creating and using Utility APIs including:
- API interface definition patterns
- Creating API references with
createApiRef - Implementing default API classes
- Registering with
ApiBlueprint - Using APIs in components with
useApi - Common API patterns (REST clients, caching, event bus)
- Testing strategies
Testing
- ✅ Testing Reference — Comprehensive testing guide including:
- Testing React components with
renderInTestAppand theapisoption - Testing extensions with
createExtensionTester(...).reactElement() mockApis.*helpers andTestApiProviderfor standalone rendering- Testing permissions and entity components
- Routes and navigation testing
- Best practices and common patterns
- Testing React components with
External References
- Building Frontend Plugins — NFS plugin authoring.
- Extension Blueprints — Framework reference.
- Common Extension Blueprints — Cheat sheet for all built-in blueprints.
- Frontend System Testing — Canonical testing guide.