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.

1. Context & Prerequisites

Before scaffolding, ensure your Backstage monorepo is initialized and running on Node.js 18+. 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:

npx @backstage/create-plugin

Select frontend as the plugin type and provide a unique ID (e.g., my-dashboard). 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 App component render
<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 -p plugins/my-dashboard

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 @material-ui/icons export. 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

FAQ

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? Yes, frontend plugins are bundled as static assets. However, they require the core Backstage app to be rebuilt or dynamically loaded via the plugin registry. Independent deployment is only viable if your architecture supports micro-frontends with runtime plugin fetching.

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 correctly aliases React to prevent duplicate instances.