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.tsthat callscreateBackend()). - 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.
Exact Configuration
-
Scaffold the plugin with the CLI.
# Requires @backstage/cli >= 0.26 yarn new --select backend-plugin # name it, e.g. "healthcheck" -
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 })); }, }); }, }); -
Add the plugin to the backend — one line, no manual wiring.
// packages/backend/src/index.ts backend.add(import('@internal/plugin-healthcheck-backend'));
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
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 |
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.