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. 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, configure the proxy in app-config.yaml to bypass browser CORS restrictions:

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. 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. 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.