Step-by-step guide to creating a Backstage frontend plugin

Platform engineers frequently require tailored UI components to surface internal metrics, custom dashboards, or domain-specific workflows within their developer portals. This guide provides a precise, step-by-step workflow for scaffolding, configuring, and validating a custom frontend plugin using the official Backstage CLI. By following this structured approach, you will integrate a production-ready React component into the core application while adhering to the Plugin Ecosystem & Custom Extensions architecture standards.

Frontend plugin scaffold to running route The CLI generates a plugin package exporting a routeRef and page, which App.tsx binds inside FlatRoutes so the dev server serves the route. backstage-cli new plugins/my-dashboard plugin.ts routeRef + Page App.tsx FlatRoutes bind yarn dev /my-dashboard the routeRef export is what makes dynamic binding work
Each step hands off one artifact: the CLI scaffolds the package, plugin.ts exports the routeRef, App.tsx binds it, and the dev server serves the route.

1. Context & Prerequisites

Before scaffolding, ensure your Backstage monorepo is initialized and running on Node.js 20+. Frontend plugins operate as isolated React packages that communicate with the core app via routing and shared APIs. Understanding the Building Custom Backstage Plugins methodology is essential for maintaining type safety and avoiding dependency collisions during the build process. Three conditions must hold before scaffolding: a supported Node runtime, a monorepo whose core app already builds, and a package manager matching the lockfile.

Baseline before scaffolding a frontend plugin A supported Node runtime, a cleanly building core app, and a matching package manager enable scaffolding. Node 20+ matching pkg manager core app builds clean Ready to scaffold cli new plugin
A core app that already builds cleanly isolates any new error to the plugin you are adding.

Verify your environment meets the following baseline:

  • node --version returns v18.x or higher
  • yarn --version or npm --version matches your monorepo lockfile manager
  • Core app builds successfully via yarn build

2. Exact Configuration & Scaffolding

Execute the official CLI command from the repository root:

yarn backstage-cli new --select plugin

Provide a unique ID (e.g., my-dashboard) when prompted. The CLI generates a structured package under plugins/my-dashboard.

Update plugins/my-dashboard/src/plugin.ts to export the default createPlugin instance:

import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';

export const myPlugin = createPlugin({
  id: 'my-dashboard',
  routes: {
    root: rootRouteRef,
  },
});

export const MyDashboardPage = myPlugin.provide(
  createRoutableExtension({
    name: 'MyDashboardPage',
    component: () => import('./components/MyDashboardPage').then(m => m.MyDashboardPage),
    mountPoint: rootRouteRef,
  }),
);

Register the route in packages/app/src/App.tsx by importing the plugin and binding it within <FlatRoutes>:

import { MyDashboardPage } from '@internal/plugin-my-dashboard';
// ... inside AppRoutes
<FlatRoutes>
  <Route path="/my-dashboard" element={<MyDashboardPage />} />
</FlatRoutes>

If your plugin fetches external data, route it through the Backstage proxy rather than calling the API directly from the browser. The proxy runs server-side, so it sidesteps CORS entirely and keeps the API token out of the client bundle.

Proxying external calls to avoid CORS The plugin calls the backend proxy path, which forwards server-side to the external API with an injected token. Plugin (browser) fetch /api/proxy/my-api Backend proxy inject token External API no CORS, no token leak
The browser only ever talks to same-origin /api/proxy; the credentialed call happens server-side.
proxy:
  '/my-api':
    target: 'https://api.internal.corp'
```yaml
proxy:
  '/my-api':
    target: 'https://api.internal.corp'
    changeOrigin: true
    secure: true
    headers:
      Authorization: ${API_TOKEN}

3. Validation & Local Testing

Run the development server from the monorepo root:

yarn dev

The Backstage dev server will compile the plugin and inject it into the frontend bundle. A healthy plugin passes four checks: the route renders, proxied calls flow through /api/proxy, the console is free of duplicate-React warnings, and lint and type-check are clean.

Frontend plugin local-validation checklist Route renders, proxied API calls route correctly, the console is clean, and lint and type-check pass. Route renders no 404 / hydration Proxy routes /api/proxy/my-api Console clean no dup React Lint + tsc clean
A duplicate-React warning in the console is the classic symptom of a missing peer-dependency declaration.

Navigate to http://localhost:3000/my-dashboard to verify the route resolves. Open browser developer tools to confirm zero console warnings regarding missing peer dependencies or React version mismatches. Use yarn tsc to validate TypeScript compilation across the new plugin package:

yarn tsc --noEmit

Validation Checklist:

  • Route renders without HTTP 404 or hydration errors
  • Network tab confirms proxied API calls route through http://localhost:7007/api/proxy/my-api
  • Console remains clean of Invalid hook call or duplicate React instance warnings
  • yarn lint passes with zero critical violations

4. Edge Cases & Production Hardening

Common deployment failures stem from unhandled CORS policies when the plugin calls external APIs, or missing @backstage/core-plugin-api peer dependencies in package.json. The three failure modes each have a fast, specific recovery, shown below.

Frontend plugin failure modes and recoveries Crash on load, missing dependencies, and CI failures each map to a specific recovery step. crash on load missing dependency CI type errors revert route + clear cache add to peerDependencies tsc --noEmit locally first
Declaring @backstage/core-plugin-api as a peer dependency prevents the duplicate-React instance behind most load crashes.

If the sidebar icon fails to render, verify that the icon prop in App.tsx uses a valid @mui/icons-material export (Backstage uses MUI v5). If the sidebar icon fails to render, verify that the icon prop in App.tsx uses a valid @mui/icons-material export (Backstage uses MUI v5). For monorepo builds, ensure tsconfig.json references the plugin correctly to prevent module resolution errors during CI/CD.

Rapid Resolution & Rollback:

  • Crash on load: Revert packages/app/src/App.tsx route registration, clear build artifacts (rm -rf node_modules/.cache packages/app/dist), and rebuild.
  • Missing dependencies: Add @backstage/core-plugin-api to peerDependencies in plugins/my-dashboard/package.json and run yarn install.
  • CI pipeline failure: Run yarn tsc --noEmit and yarn build:backend locally to isolate strict-mode type errors before merging.

Common Pitfalls

  • Omitting peer dependency declarations in package.json causing runtime React version conflicts
  • Hardcoding API endpoints instead of using the Backstage proxy configuration
  • Failing to export the plugin routeRef from the plugin package, breaking dynamic routing
  • Ignoring TypeScript strict mode during scaffolding, leading to silent build failures in CI

Frequently Asked Questions

How do I share state between my new frontend plugin and core Backstage components?

Use the @backstage/core-plugin-api to create custom extension data refs or leverage the existing identityApi and configApi for cross-plugin communication without breaking encapsulation.

Can I deploy the frontend plugin independently of the backend?

Frontend plugins are bundled as static assets compiled into the core Backstage app. Independent deployment requires the entire Backstage app to be rebuilt. Runtime plugin loading is not supported in standard Backstage; all plugins are compiled in at build time.

Why does my plugin fail to load in production builds?

Production builds often strip development-only dependencies. Verify that all @backstage/* packages are listed under peerDependencies and devDependencies, and that your Webpack configuration (managed by @backstage/cli) correctly aliases React to prevent duplicate instances.