Creating a Backend Plugin with the New Backend System

Backstage’s new backend system replaced the old hand-wired createRouter boilerplate with a declarative plugin-and-module model, and starting a backend plugin the modern way saves you a painful migration later. This how-to scaffolds a backend plugin that registers itself, declares its dependencies, and exposes a route — all through the new system. It extends Building Custom Backstage Plugins within Plugin Ecosystem & Custom Extensions.

Prerequisites

  • Backstage 1.20+ on the new backend system (a packages/backend/src/index.ts that calls createBackend()).
  • Node.js 20+ and the Backstage CLI available for scaffolding.
  • Familiarity with the core services — logger, database, http-router — that plugins depend on by reference.
  • A clear, single responsibility for the plugin, so it stays a plugin rather than sprawling into several.
Prerequisites for a backend plugin The new backend system, the CLI, core-service familiarity, and a single responsibility enable a clean plugin. new backend system Backstage CLI core services known single responsibility Clean plugin self-registering
The new system injects core services by reference, so the plugin declares what it needs rather than importing wiring — confirm you are on it before starting.

Exact Configuration

  1. Scaffold the plugin with the CLI.

    # Requires @backstage/cli >= 0.26
    yarn new --select backend-plugin
    # name it, e.g. "healthcheck"
    
  2. Define the plugin with createBackendPlugin, declaring dependencies and registering an init.

    // plugins/healthcheck-backend/src/plugin.ts
    import { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';
    
    export const healthcheckPlugin = createBackendPlugin({
      pluginId: 'healthcheck',
      register(env) {
        env.registerInit({
          deps: { http: coreServices.httpRouter, logger: coreServices.logger },
          async init({ http, logger }) {
            http.use(await createRouter({ logger }));
          },
        });
      },
    });
    
  3. Add the plugin to the backend — one line, no manual wiring.

    // packages/backend/src/index.ts
    backend.add(import('@internal/plugin-healthcheck-backend'));
    
Plugin registration flow The plugin declares dependencies, the backend injects the core services, and init wires the router. plugin declares deps backend injects core services init runs wires router route live /api/healthcheck
Because dependencies are declared, the backend can order plugin startup for you — no more hand-managing the wiring sequence.

Validation

# Requires a running backend, curl
# 1. The plugin registered without wiring errors
yarn workspace backend start 2>&1 | grep -i "healthcheck" | head -1
# Expected: an init log line, no "failed to resolve dependency"

# 2. The route responds
curl -s "${PORTAL_URL}/api/healthcheck/health" | jq '.status'
# Expected: "ok"

# 3. It uses injected services, not ad-hoc globals
grep -q "coreServices" plugins/healthcheck-backend/src/plugin.ts && echo "uses core services"
# Expected: uses core services
Backend plugin validation checks The plugin registers cleanly, its route responds, and it consumes injected core services. Registers clean deps resolved Route responds status ok Injected services not globals
A dependency-resolution error at startup is the new system telling you a declared service is missing — read it before reaching for the old wiring.

Edge Cases & Troubleshooting

Symptom Root Cause Resolution
“Failed to resolve dependency” A declared service is not provided Add the missing module or correct the deps reference
Route 404s Plugin not added to the backend Add backend.add(import(...)) for the plugin
Old createRouter boilerplate Copied from a legacy plugin Migrate to createBackendPlugin and injected deps
Config not read No config service dependency declared Add coreServices.rootConfig to deps
Startup order flakiness Manual sequencing attempted Let the system order via declared deps; remove manual awaits
Backend plugin pitfalls by class Unresolved deps and 404 routes are registration issues; legacy boilerplate and manual ordering are migration issues. Registration unresolved dep route 404 → add module + backend.add Migration legacy boilerplate manual ordering → declare deps, drop wiring
Fighting the startup order by hand is a sign you are still thinking in the old model — declare the dependency and let the system sequence it.

Frequently Asked Questions

When should this be a module instead of a plugin?

A plugin owns an ID and its routes; a module extends an existing plugin — adding a catalog processor, a scaffolder action, or an auth provider. If your code stands alone with its own API, it is a plugin; if it plugs into someone else’s plugin, it is a module. Choosing wrong means either a plugin that cannot reach what it needs to extend, or a module with nowhere to attach.

Do I still need createRouter?

You still build an Express router for HTTP routes, but you register it through the injected httpRouter service rather than wiring it into the backend by hand. The router itself is familiar; what changed is how it gets mounted — declaratively, through the plugin’s init, instead of by manual assembly in index.ts.