Build Backstage backend plugins with createBackendPlugin and core services: DI, httpRouter, secure-by-default auth, Knex DB, routes, testing. Use for APIs and background jobs.
This skill provides specialized knowledge and workflows for building Backstage backend plugins using the New Backend System. It guides the development of server-side functionality including REST/HTTP APIs, background jobs, data processing, and integrations.
Use this skill when creating server-side functionality for Backstage: REST/HTTP APIs, background jobs, data processing, or integrations.
Before building a backend plugin, clearly understand:
Load reference files as needed based on the plugin requirements:
For Core Services:
For Testing:
Follow the Golden Path workflow below for implementation, referring to reference files as needed.
Important Decisions:
After implementing the plugin:
startTestBackendTestDatabasesyarn backstage-cli package test --coverage
Before publishing:
yarn new → backend‑plugin; the package lives in plugins/<id>-backend/.createBackendPlugin, declare dependencies via deps, and initialize in register(env).registerInit.coreServices.httpRouter. Backstage prefixes plugin routers with /api/<pluginId>.httpRouter.addAuthPolicy({ path, allow }) to allow unauthenticated endpoints like /health.coreServices.Validate inputs at the edge using a schema (e.g., zod) before hitting DBs or external services:
import { z } from 'zod';
const querySchema = z.object({ q: z.string().min(1) });
router.get('/search', async (req, res, next) => {
const parsed = querySchema.safeParse(req.query);
if (!parsed.success) return res.status(400).json({ error: 'invalid query' });
try {
// ... perform work with parsed.data.q
res.json({ items: [] });
} catch (e) { next(e); }
});
Add a terminal error handler to your router and prefer structured logs with context:
import { errorHandler } from '@backstage/backend-common';
router.use(errorHandler());
Keep backends secure-by-default
Open only explicit paths with addAuthPolicy
For protected routes, extract credentials with httpAuth
Derive user/entity identity via userInfo when required
// Inside a route handler
const creds = await httpAuth.credentials(req, { allow: ['user', 'service'] });
const { userEntityRef } = await userInfo.getUserInfo(creds);
logger.info('request', { userEntityRef });
const knex = await database.getClient(); to get a database client.js) and export them in package.json (see Core Services Reference for details)logger.child({ plugin: 'example' }) for traceability# 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 with createBackendPlugin.
src/plugin.ts — plugin + DI + routerimport { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
export const examplePlugin = createBackendPlugin({
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
httpRouter: coreServices.httpRouter,
logger: coreServices.logger,
database: coreServices.database, // optional
httpAuth: coreServices.httpAuth, // optional
userInfo: coreServices.userInfo, // optional
},
async init({ httpRouter, logger, database, httpAuth, userInfo }) {
const router = await createRouter({ logger, database, httpAuth, userInfo });
httpRouter.use(router);
// Secure-by-default: open /health only
httpRouter.addAuthPolicy({ path: '/health', allow: 'unauthenticated' });
},
});
},
});
export { examplePlugin as default } from './plugin';
Key points: DI via deps, register routes with the plugin’s httpRouter, and export the plugin as the default. (Backstage)
src/service/router.ts — minimal Express routerimport express from 'express';
import type {
LoggerService,
DatabaseService,
HttpAuthService,
UserInfoService,
} from '@backstage/backend-plugin-api';
export interface RouterOptions {
logger: LoggerService;
database?: DatabaseService;
httpAuth?: HttpAuthService;
userInfo?: UserInfoService;
}
export async function createRouter(options: RouterOptions): Promise<express.Router> {
const { logger } = options;
const router = express.Router();
router.get('/health', (_req, res) => {
logger.info('health check');
res.json({ status: 'ok' });
});
return router;
}
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/health returns { "status": "ok" }. (Backstage)
coreServices.database to get a Knex client; create your own migrations and run them via your chosen process. (Backstage)coreServices.httpAuth + coreServices.userInfo to obtain the calling user and their entity refs when an endpoint needs identity. (Backstage)packages/backend/src/index.ts via backend.add(import('@internal/plugin-<id>-backend')).yarn start at the root). Then check:{ "status": "ok" }/health with addAuthPolicy.Use Backstage's CLI for tests and lints:
yarn backstage-cli package test
yarn backstage-cli package lint
yarn backstage-cli repo lint
Keep routers small (/service/router.ts), inject dependencies (DB, auth, clients) from plugin.ts, and avoid in-memory state (horizontally scalable).
| Problem | Solution | Reference |
|---|---|---|
404s under /api |
Remember Backstage prefixes plugin routers with /api/<pluginId> |
Backstage |
| Auth unexpectedly required | Backends are secure by default; open endpoints explicitly via httpRouter.addAuthPolicy |
Backstage |
| Tight coupling | Never call other backend code directly; communicate over the network or through well-defined services | Backstage |
Load these resources as needed during development:
startTestBackendTestDatabaseshttpRouter, /api/<pluginId>, secure‑by‑default, DB & identity usage. (Backstage)