
backstage-backend-plugin
Build Backstage backend plugins with createBackendPlugin and core services: DI, httpRouter, secure-b
Backstage Backend Plugin (New Backend System)
About This Skill
This skill provides specialized knowledge and workflows for building Backstage backend plugins and modules using the New Backend System. It guides the development of server-side functionality including REST/HTTP APIs, background jobs, data processing, and integrations, plus the module/extension-point pattern used to extend other plugins.
What This Skill Provides
- Specialized workflows - Step-by-step procedures for creating and configuring backend plugins and modules
- Best practices - Production-ready patterns for auth, validation, error handling, and database usage
- Golden path templates - Copy/paste code snippets for common backend patterns
- Domain expertise - Backstage-specific knowledge about DI, core services, and secure-by-default architecture
When to Use This Skill
Use this skill when creating server-side functionality for Backstage: REST/HTTP APIs, background jobs, data processing, integrations, or modules that extend an existing plugin's extension points.
Development Workflow
Phase 1: Planning and Research
1.1 Understand the Requirements
Before building a backend plugin, clearly understand:
- What endpoints or APIs are needed (REST, GraphQL, webhooks)
- What data storage requirements exist (database, cache)
- Whether background jobs or scheduled tasks are needed
- Authentication and authorization requirements
- Whether you're writing a plugin (new surface area) or a module (extending another plugin's extension points)
- Integration points with other services or plugins
1.2 Load Reference Documentation
Load reference files as needed based on the plugin requirements:
For Core Services:
- ⚙️ Core Services Reference - Comprehensive guide to all core backend services including:
- httpRouter, logger, database, httpAuth, userInfo, auth
- cache, scheduler, urlReader, discovery, permissions, permissionsRegistry
- auditor, lifecycle, rootHealth, rootConfig, rootInstanceMetadata
- Usage patterns and best practices for each service
For Testing:
- ✅ Testing Reference - Comprehensive testing guide for backend plugins
Phase 2: Implementation
Follow the Golden Path workflow below for implementation, referring to reference files as needed.
Important Decisions:
- Determine which core services are needed (load ⚙️ Core Services Reference)
- Plan database schema and migrations if using database
- Design authentication policies (which endpoints, if any, should be unauthenticated?)
- Plan error handling and validation strategies
- Decide whether extension points are needed so other teams can extend this plugin
Phase 3: Testing
After implementing the plugin:
- Load the ✅ Testing Reference
- Write comprehensive tests for:
- Plugin/module integration via
startTestBackend - Router endpoints using
supertestagainst the test server - Database operations with
TestDatabases - External service calls with MSW (v2 API)
- Authentication flows using
mockCredentialsandmockServices.httpAuth.mock
- Plugin/module integration via
- Run tests and achieve good coverage:
yarn backstage-cli package test --coverage --watchAll=false
Phase 4: Review and Polish
Before publishing:
- Run linting and structure checks
- Ensure proper error handling throughout — let errors bubble up so the root HTTP router's error middleware formats them
- Verify authentication policies are correct (backends are secure by default)
- Check database migrations are production-ready and written in JavaScript
- Review the Common Pitfalls section below
- Test horizontal scalability (no in-memory state; use the
cacheordatabaseservices)
Quick Facts
- Scaffold with
yarn new→ selectbackend-plugin; the package lives inplugins/<id>-backend/. - Define the plugin with
createBackendPlugin, declare dependencies viadeps, and initialize inregister(env).registerInit. - Attach routes through
coreServices.httpRouter. Backstage prefixes plugin routers with/api/<pluginId>. - Backends are secure by default; use
httpRouter.addAuthPolicy({ path, allow })to allow unauthenticated endpoints. allowonaddAuthPolicyonly accepts'unauthenticated'or'user-cookie'.- Core services (
logger,database,httpRouter,httpAuth,auth,userInfo,urlReader,scheduler,cache,permissions, etc.) are available viacoreServices. - Validate inputs with
zodand throwInputErrorfrom@backstage/errors. Let the root HTTP router handle error responses — you usually don't need per-router error middleware. - A module (
createBackendModule) extends another plugin via its extension points. A plugin exposes extension points withenv.registerExtensionPoint(...).
Production Best Practices
Request Validation
Validate inputs at the edge using zod. Throw InputError (from @backstage/errors) on failure so the root error handler can serialize it as a 400 response — no need to write the response yourself:
import { InputError } from '@backstage/errors';
import { z } from 'zod/v3';
const querySchema = z.object({ q: z.string().min(1) });
router.get('/search', async (req, res) => {
const parsed = querySchema.safeParse(req.query);
if (!parsed.success) {
throw new InputError(parsed.error.toString());
}
const results = await search(parsed.data.q);
res.json({ items: results });
});
Use zod/v3 (or zod/v4 — both are available in the repo today) to pin behaviour across the codebase. Recent scaffolded plugins use zod/v3.
Error Handling
The root HTTP router installs a centralized error middleware, so individual plugin routers don't need to add their own. If you need to customize error formatting at the root level, use MiddlewareFactory from @backstage/backend-defaults/rootHttpRouter:
import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';
// Typically only set up once in tests or at the root.
const middleware = MiddlewareFactory.create({ config, logger });
router.use(middleware.error({ logAllErrors: false }));
The legacy helper
errorHandlerfrom@backstage/backend-commonhas been removed. UseMiddlewareFactory.create(...).error(), ormockErrorHandlerfrom@backstage/backend-test-utilsin tests.
Wrap async handlers with express-promise-router so thrown errors reach the error middleware automatically — that's what the scaffolded template does.
Auth and Identity
- Backends are secure-by-default
- Open specific paths explicitly with
httpRouter.addAuthPolicy({ path, allow: 'unauthenticated' }) - For protected routes, extract credentials with
httpAuth - Derive user/entity identity via
userInfowhen required - Use
authto issue on-behalf-of tokens for calls to other plugins
router.get('/me', async (req, res) => {
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
const { userEntityRef, ownershipEntityRefs } = await userInfo.getUserInfo(credentials);
res.json({ userEntityRef, ownershipEntityRefs });
});
Database Usage
const knex = await database.getClient();gets a per-plugin Knex client (separate schema per plugin)- Keep queries in small repo/service modules
- Write migrations in JavaScript (
.js) and include them in your package'sfileslist - Run migrations via
knex.migrate.latest({ directory, migrationSource })duringinit
Observability & Scalability
- Avoid in-memory state — use the
cacheordatabaseservices so multiple replicas share state - Make scheduled task handlers idempotent
- Log with
logger.child({ plugin: 'example' })for traceability - For auditing, use
coreServices.auditorto emit structured audit events
Golden Path (Copy/Paste Workflow)
1) Scaffold
# From the repository root (interactive)
yarn new
# Select: backend-plugin
# Plugin id (without -backend suffix), e.g. example
# Non-interactive (for AI agents/automation)
yarn new --select backend-plugin --option pluginId=example --option owner=""
This creates plugins/example-backend/ using the New Backend System. The scaffolded structure is:
src/plugin.ts—createBackendPluginwiringsrc/router.ts— Express router (usesexpress-promise-router)src/services/— service refs + implementationssrc/index.ts— re-exports the plugin as defaultdev/index.ts— local dev backend viacreateBackendsrc/plugin.test.ts— integration tests usingstartTestBackend+supertest
2) src/plugin.ts — plugin + DI + router
The scaffolded shape wires in a service ref (e.g. todoListServiceRef) alongside core services:
import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { createRouter } from './router';
import { todoListServiceRef } from './services/TodoListService';
export const examplePlugin = createBackendPlugin({
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
httpAuth: coreServices.httpAuth,
httpRouter: coreServices.httpRouter,
todoList: todoListServiceRef,
},
async init({ httpAuth, httpRouter, todoList }) {
httpRouter.use(await createRouter({ httpAuth, todoList }));
// Backends are secure-by-default. Open specific endpoints explicitly:
httpRouter.addAuthPolicy({ path: '/health', allow: 'unauthenticated' });
},
});
},
});
Add more core services to deps as you need them (e.g. logger, database, userInfo, auth, scheduler).
3) src/index.ts — default export
export { examplePlugin as default } from './plugin';
Keep this line only in
index.ts. Don't duplicate it insideplugin.ts.
4) src/router.ts — minimal Express router
Use express-promise-router so thrown errors propagate to the root error handler. Validate request bodies with zod. Check user credentials inline with httpAuth.credentials:
import type { HttpAuthService } from '@backstage/backend-plugin-api';
import { InputError } from '@backstage/errors';
import express from 'express';
import Router from 'express-promise-router';
import { z } from 'zod/v3';
import type { todoListServiceRef } from './services/TodoListService';
export async function createRouter({
httpAuth,
todoList,
}: {
httpAuth: HttpAuthService;
todoList: typeof todoListServiceRef.T;
}): Promise<express.Router> {
const router = Router();
router.use(express.json());
const todoSchema = z.object({
title: z.string(),
entityRef: z.string().optional(),
});
router.post('/todos', async (req, res) => {
const parsed = todoSchema.safeParse(req.body);
if (!parsed.success) {
throw new InputError(parsed.error.toString());
}
const result = await todoList.createTodo(parsed.data, {
credentials: await httpAuth.credentials(req, { allow: ['user'] }),
});
res.status(201).json(result);
});
router.get('/todos', async (_req, res) => {
res.json(await todoList.listTodos());
});
router.get('/todos/:id', async (req, res) => {
res.json(await todoList.getTodo({ id: req.params.id }));
});
return router;
}
5) Add to your backend
In packages/backend/src/index.ts:
const backend = createBackend();
backend.add(import('@internal/plugin-example-backend'));
backend.start();
Now GET http://localhost:7007/api/example/todos returns an empty list. addAuthPolicy allowed /health through unauthenticated; every other path requires a valid user or service credential.
6) Services, DB, auth, identity (when needed)
- Service refs: define with
createServiceRef<T>({ id: 'example.todoList' }), register withcreateServiceFactory({ service, deps, factory }). Pattern in the scaffold:services/TodoListService.tsexports the ref plustodoListServiceFactorywhich is registered via the plugin'sregister(env). - Database: depend on
coreServices.databaseto get a Knex client; run your own migrations withknex.migrate.latest(...)ininit. - Identity: use
coreServices.httpAuth+coreServices.userInfoto obtain the calling user and their entity refs. - Inter-plugin calls: use
coreServices.authto mint a token on behalf of the current caller, thencoreServices.discoveryfor the target plugin's base URL — see Core Services reference. - Full catalog of core services: see ⚙️ Core Services Reference.
Modules and extension points
Modules are packages that extend an existing plugin at a pre-defined integration point. Use them to add processors, providers, actions, etc., without forking the plugin.
Exposing an extension point (in a plugin):
import {
createBackendPlugin,
createExtensionPoint,
} from '@backstage/backend-plugin-api';
interface ExampleProcessor {
process(value: string): Promise<string>;
}
export interface ExampleProcessingExtensionPoint {
addProcessor(p: ExampleProcessor): void;
}
export const exampleProcessingExtensionPoint =
createExtensionPoint<ExampleProcessingExtensionPoint>({
id: 'example.processing',
});
export const examplePlugin = createBackendPlugin({
pluginId: 'example',
register(env) {
const processors: ExampleProcessor[] = [];
env.registerExtensionPoint(exampleProcessingExtensionPoint, {
addProcessor(p) {
processors.push(p);
},
});
env.registerInit({
deps: { /* ... */ },
async init() {
// Use `processors` during plugin setup.
},
});
},
});
Extending a plugin (in a module):
import { createBackendModule } from '@backstage/backend-plugin-api';
import { exampleProcessingExtensionPoint } from '@example/plugin-example-node';
export const exampleModuleCustomProcessor = createBackendModule({
pluginId: 'example',
moduleId: 'custom-processor',
register(env) {
env.registerInit({
deps: { example: exampleProcessingExtensionPoint },
async init({ example }) {
example.addProcessor(new MyCustomProcessor());
},
});
},
});
// src/index.ts
export { exampleModuleCustomProcessor as default } from './module';
Scaffold a module with yarn new → backend-module. Generated packages live in plugins/<pluginId>-backend-module-<moduleId>/.
Verify in a Backstage backend
- Add the plugin to
packages/backend/src/index.tsviabackend.add(import('@internal/plugin-<id>-backend')). - Start the repo (
yarn start). Then check:GET http://localhost:7007/api/example/health→{ "status": "ok" }(if your plugin exposes a/healthendpoint and opens it withaddAuthPolicy).- If you get a 401, the request hit an authenticated endpoint without a valid token — either authenticate, or open the path with
addAuthPolicy.
Testing, linting & structure checks
# Always pass --watchAll=false when running non-interactively (CI, AI agents);
# without it jest starts in watch mode and never exits.
yarn backstage-cli package test --watchAll=false
yarn backstage-cli package lint
yarn backstage-cli repo lint
Keep routers small (/router.ts), inject dependencies (DB, auth, clients) from plugin.ts, and avoid in-memory state so the backend stays horizontally scalable.
Common Pitfalls (and Fixes)
| Problem | Solution |
|---|---|
404s under /api | Backstage prefixes plugin routers with /api/<pluginId>. Don't add the prefix yourself in your route definitions. |
| Auth unexpectedly required | Backends are secure by default; open endpoints explicitly via httpRouter.addAuthPolicy({ path, allow: 'unauthenticated' }). |
| Tight coupling between plugins | Never import other backend plugins' internals. Communicate over HTTP using discovery + auth (auth.getPluginRequestToken({ onBehalfOf, targetPluginId })), or use extension points/modules. |
errorHandler not found | @backstage/backend-common has been removed. Use MiddlewareFactory.create({ config, logger }).error() from @backstage/backend-defaults/rootHttpRouter, or rely on the root error handler that's installed automatically. |
| Tests hang or timeout | Make sure startTestBackend is awaited, and use await request(server).get(...) (from supertest), not server.request(...). |
| MSW handlers stop matching | MSW v2 uses http.get(url, () => HttpResponse.json(...)), not rest.get. Update imports and handler signatures. |
| Migrations don't run in prod | Migrations must be .js files listed in your package.json files array, so they're copied into the published artifact. |
Reference Files
📚 Documentation Library
Load these resources as needed during development:
Core Services
- ⚙️ Core Services Reference - Complete guide to all core backend services including:
- HTTP Router / Root HTTP Router for route registration
- Logger / Root Logger for structured logging
- Database Service for Knex-based data access
- HTTP Auth + User Info + Auth services
- Cache, Scheduler, URL Reader, Discovery services
- Permissions + Permissions Registry for authorization
- Auditor, Lifecycle, Root Config, Root Health, Plugin Metadata
- Service composition examples and best practices
Testing
- ✅ Testing Reference - Comprehensive testing guide including:
- Testing backend plugins with
startTestBackend - The
mockServicescatalog andmockCredentialshelpers - Testing routers with
supertest(usingawait request(server).get(...)) - Testing authentication, permissions, and scheduled tasks
- External service mocking with MSW v2
- Database testing with
TestDatabases(from@backstage/backend-test-utils) - Service factory testing, module testing, and integration patterns
- Testing backend plugins with
External References
- Backend Plugins and Modules — authoring plugins and modules.
- Core Backend Services — index of all core services.
- Testing Backend Plugins — official testing guide.
- Migrating Backends — legacy-to-new-system migration (covers
errorHandler→MiddlewareFactory).