Building backend plugins in Backstage with the New Backend System: createBackendPlugin and createBackendModule, service APIs, plugin options, and testing with backend-test-utils. Closing with how to integrate plugins with the proxy, auth, catalog, and scaffolder backends.

In episode 17 you built plugins on the frontend side with the New Frontend System: creating extensions, wiring pages and components into the entity page. Episode 18 completes the story from the server side. Backstage isn't just a collection of UI — behind almost every frontend plugin there's usually a backend plugin providing data, HTTP endpoints, and a bridge into internal systems. Here you'll learn the New Backend System: defining plugins and modules, using service APIs, setting plugin options, testing everything, then wiring it into existing backends like proxy, auth, catalog, and scaffolder.
The New Backend System is the architectural foundation of Backstage backend plugins. Previously every plugin was wired directly inside the backend code, registering itself by calling APIs from app-backend. Now plugins and modules are declarative: you write plugin definitions, then assemble them like Lego at a single entry point called backend.
There are three main building units to understand:
| Unit | Purpose | When to use |
|---|---|---|
createBackendPlugin | Creates a standalone backend plugin | Every time there's a new backend plugin |
createBackendModule | Injects features into other plugins | Cross-plugin integrations, extra custom logic |
| Service APIs | Shared services used across plugins | Accessing config, database, logger, auth, catalog |
All three work together: plugins provide features, modules extend them, and services channel the same functions to all plugins.
createBackendPlugin is the function for defining a backend plugin. A minimal backend plugin has a unique pluginId and a register function that connects the plugin to the backend:
import { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';
export const healthPlugin = createBackendPlugin({
pluginId: 'health',
register(env) {
env.registerInit({
deps: { logger: coreServices.logger, httpRouter: coreServices.httpRouter },
async init({ logger, httpRouter }) {
httpRouter.use('/healthz', (_req, res) => {
logger.info('health check dipanggil');
res.json({ status: 'ok' });
});
},
});
},
});This plugin provides a new HTTP endpoint via httpRouter. Once installed at the backend entry point with backend.add, its router and logger are available immediately without extra setup.
A module is a unit that extends another plugin. The common pattern is registering into an extension point owned by the target plugin:
import { createBackendModule } from '@backstage/backend-plugin-api';
export const customScaffolderModule = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'custom-actions',
register(env) {
env.registerInit({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
logger: coreServices.logger,
},
async init({ scaffolder, logger }) {
scaffolder.addActions(new MyCustomAction({ logger }));
},
});
},
});A module binds to the pluginId of the plugin it extends. The example above adds a new action to the scaffolder without touching the scaffolder plugin's own code. This is the form of extensibility that makes Backstage easy to customize without forking the repository.
Service APIs are the service contracts Backstage provides: config, logger, database, cache, auth, httpAuth, httpRouter, catalog, permissions, and many more. A plugin requests services through a deps declaration rather than importing an implementation directly — Backstage decides how that service is realized, including how it behaves under testing.
For your own services, register them with createServiceFactory:
import { createServiceRef, createServiceFactory } from '@backstage/backend-plugin-api';
export const reportingServiceRef = createServiceRef<ReportingService>({ id: 'acme.reporting' });
export const reportingServiceFactory = createServiceFactory({
service: reportingServiceRef,
deps: { logger: coreServices.logger, config: coreServices.rootConfig },
async factory({ logger, config }) {
return new ReportingService({ logger, endpoint: config.getString('reporting.url') });
},
});A custom service registered with createServiceFactory can be requested by other plugins via deps — a clean way to share database connections, HTTP clients, or internal helpers across plugins.
Backend plugins often need specific settings. The New Backend System supports plugin options defined in the plugin factory function:
import { createBackendPlugin } from '@backstage/backend-plugin-api';
import { z } from 'zod';
export const reportingPlugin = createBackendPlugin({
pluginId: 'reporting',
options: {
endpoint: z.string().url(),
retries: z.number().default(3),
},
async register(env, options) {
env.registerInit({
deps: { httpRouter: coreServices.httpRouter },
async init({ httpRouter }) {
httpRouter.use('/reports', new ReportsRouter(options));
},
});
},
});When installed, the option values are passed along with the backend.add call:
backend.add(reportingPlugin, { endpoint: 'https://reports.internal', retries: 5 });With options, one plugin can be reused with different configurations without duplicating code.
@backstage/backend-test-utils provides mock services and a harness for running a test backend without a real server:
import { startTestBackendFromFeatures, mockServices } from '@backstage/backend-test-utils';
const backend = await startTestBackendFromFeatures({
features: [
reportingPlugin,
mockServices.config.factory({
data: { reporting: { url: 'https://test.internal' } },
}),
],
});This kind of harness also provides setupRequestHandlerContext for testing specific HTTP routes. With the test harness, a plugin is tested in realistic isolation: the services it needs are mocked, and only the plugin under test is active.
The main advantage of the plugin architecture is easy integration. Common needs:
httpAuth or auth; Backstage manages the tokens, sessions, and sign-in providers.catalog service, or adding your own processors for custom data.All of these integrations are expressed through service APIs and modules, so you don't need to hack into another plugin's code.
Tip
The most common pattern for backend integration: module + extension point. If the target plugin exposes an extension point, register through a module instead of changing global configuration. This keeps plugins declarative and makes upgrading Backstage versions easier later.
In this episode 18, you understood the New Backend System: building plugins with createBackendPlugin, extending other plugins with createBackendModule, using service APIs like config, logger, database, and catalog, setting plugin options, testing plugins with @backstage/backend-test-utils, and integrating with the proxy, auth, catalog, and scaffolder backends through custom service registration.
The key takeaways:
createBackendPlugin, then install via backend.add.deps; Backstage provides the services.In episode 19, you move up from one instance to many: scaling & performance. We'll set up Backstage to serve thousands of engineers — from horizontal scaling, load balancing, a shared PostgreSQL database, a task scheduler for pipelines, to bundling size optimization, memory management, and cache strategy.