Guides

Backstage Plugin

Overview

This guide shows how to build a Backstage plugin that displays LevelFour cost data. It consists of two parts:

  • Backend plugin - Express router that wraps LevelFourClient from the levelfour npm package
  • Frontend plugin - React component that fetches data from the backend and renders it

The backend plugin uses Backstage's new backend system (createBackendPlugin). The frontend uses createApiFactory with discoveryApi for backend communication.

Configuration

Add your LevelFour API key to app-config.yaml:

levelfour:
  apiKey: ${LEVELFOUR_API_KEY}

Backend Plugin

plugin.ts

The plugin entry point registers the HTTP router with Backstage's core services:

import {
    createBackendPlugin,
    coreServices,
} from "@backstage/backend-plugin-api";
import { createRouter } from "./router";

export const levelfourPlugin = createBackendPlugin({
    pluginId: "levelfour",
    register(env) {
        env.registerInit({
            deps: {
                httpRouter: coreServices.httpRouter,
                logger: coreServices.logger,
                config: coreServices.rootConfig,
            },
            async init({ httpRouter, logger, config }) {
                const router = await createRouter({ logger, config });
                httpRouter.use(router);
            },
        });
    },
});

router.ts

The router reads the API key from config, creates a LevelFourClient, and exposes endpoints at /api/levelfour/*:

import { Router } from "express";
import { LevelFourClient, LevelFourError } from "levelfour";
import type { LoggerService } from "@backstage/backend-plugin-api";
import type { Config } from "@backstage/config";

interface RouterOptions {
    logger: LoggerService;
    config: Config;
}

export async function createRouter(options: RouterOptions): Promise<Router> {
    const { logger, config } = options;
    const router = Router();

    const apiKey = config.getOptionalString("levelfour.apiKey");
    if (!apiKey) {
        logger.warn("levelfour.apiKey not configured in app-config.yaml");
    }

    const client = new LevelFourClient({ apiKey });

    router.get("/health", (_req, res) => {
        res.json({ status: "ok" });
    });

    router.get("/recommendations/summary", async (_req, res) => {
        try {
            const data = await client.recommendations.getSavingsByProvider();
            res.json(data);
        } catch (err) {
            if (err instanceof LevelFourError) {
                logger.error(`LevelFour API error: ${err.statusCode} ${err.message}`);
                res.status(err.statusCode ?? 500).json({ error: err.message });
                return;
            }
            throw err;
        }
    });

    router.get("/costs/summary", async (_req, res) => {
        try {
            const data = await client.costs.getSummary();
            res.json(data);
        } catch (err) {
            if (err instanceof LevelFourError) {
                logger.error(`LevelFour API error: ${err.statusCode} ${err.message}`);
                res.status(err.statusCode ?? 500).json({ error: err.message });
                return;
            }
            throw err;
        }
    });

    router.get("/recommendations/overview", async (_req, res) => {
        try {
            const data = await client.recommendations.getOverview();
            res.json(data);
        } catch (err) {
            if (err instanceof LevelFourError) {
                logger.error(`LevelFour API error: ${err.statusCode} ${err.message}`);
                res.status(err.statusCode ?? 500).json({ error: err.message });
                return;
            }
            throw err;
        }
    });

    return router;
}

Backend Dependencies

{
    "dependencies": {
        "@backstage/backend-plugin-api": "^1.3.0",
        "express": "^4.21.0",
        "levelfour": "^0.1.0"
    }
}

Register in Backend

In your Backstage backend's src/index.ts:

backend.add(import("@internal/plugin-levelfour-backend"));

Frontend Plugin

api.ts

Define the API interface and client that communicates with the backend plugin:

import {
    createApiRef,
    type DiscoveryApi,
    type FetchApi,
} from "@backstage/core-plugin-api";

export interface LevelFourApi {
    getRecommendationsSummary(): Promise<unknown>;
    getCostsSummary(): Promise<unknown>;
    getRecommendationsOverview(): Promise<unknown>;
}

export const levelFourApiRef = createApiRef<LevelFourApi>({
    id: "plugin.levelfour.service",
});

export class LevelFourApiClient implements LevelFourApi {
    private readonly discoveryApi: DiscoveryApi;
    private readonly fetchApi: FetchApi;

    constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {
        this.discoveryApi = options.discoveryApi;
        this.fetchApi = options.fetchApi;
    }

    private async fetch(path: string): Promise<unknown> {
        const baseUrl = await this.discoveryApi.getBaseUrl("levelfour");
        const response = await this.fetchApi.fetch(`${baseUrl}${path}`);
        if (!response.ok) {
            throw new Error(`LevelFour API error: ${response.status} ${response.statusText}`);
        }
        return response.json();
    }

    async getRecommendationsSummary(): Promise<unknown> {
        return this.fetch("/recommendations/summary");
    }

    async getCostsSummary(): Promise<unknown> {
        return this.fetch("/costs/summary");
    }

    async getRecommendationsOverview(): Promise<unknown> {
        return this.fetch("/recommendations/overview");
    }
}

plugin.ts

Register the API factory and routable extension:

import {
    createPlugin,
    createApiFactory,
    createRoutableExtension,
    discoveryApiRef,
    fetchApiRef,
} from "@backstage/core-plugin-api";
import { levelFourApiRef, LevelFourApiClient } from "./api";
import { rootRouteRef } from "./routes";

export const levelfourPlugin = createPlugin({
    id: "levelfour",
    apis: [
        createApiFactory({
            api: levelFourApiRef,
            deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
            factory: ({ discoveryApi, fetchApi }) =>
                new LevelFourApiClient({ discoveryApi, fetchApi }),
        }),
    ],
    routes: {
        root: rootRouteRef,
    },
});

export const LevelFourPage = levelfourPlugin.provide(
    createRoutableExtension({
        name: "LevelFourPage",
        component: () =>
            import("./components/CostDashboard").then((m) => m.CostDashboard),
        mountPoint: rootRouteRef,
    }),
);

CostDashboard.tsx

The dashboard component fetches recommendations and spending data in parallel:

import React, { useEffect, useState } from "react";
import { useApi } from "@backstage/core-plugin-api";
import {
    Header,
    Page,
    Content,
    InfoCard,
    Progress,
} from "@backstage/core-components";
import { levelFourApiRef } from "../api";

export const CostDashboard = () => {
    const api = useApi(levelFourApiRef);
    const [recommendations, setRecommendations] = useState<unknown>(null);
    const [spending, setSpending] = useState<unknown>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);

    useEffect(() => {
        Promise.all([
            api.getRecommendationsSummary(),
            api.getCostsSummary(),
        ])
            .then(([recs, spend]) => {
                setRecommendations(recs);
                setSpending(spend);
            })
            .catch((err) => setError(err.message))
            .finally(() => setLoading(false));
    }, [api]);

    if (loading) return <Progress />;

    if (error) {
        return (
            <Page themeId="tool">
                <Header title="Cloud Cost Optimization" />
                <Content>
                    <InfoCard title="Error">{error}</InfoCard>
                </Content>
            </Page>
        );
    }

    return (
        <Page themeId="tool">
            <Header title="Cloud Cost Optimization" subtitle="Powered by LevelFour" />
            <Content>
                <InfoCard title="Savings by Provider">
                    <pre>{JSON.stringify(recommendations, null, 2)}</pre>
                </InfoCard>
                <InfoCard title="Spending Summary">
                    <pre>{JSON.stringify(spending, null, 2)}</pre>
                </InfoCard>
            </Content>
        </Page>
    );
};

Register in App

In your Backstage app's App.tsx, add the route:

import { LevelFourPage } from "@internal/plugin-levelfour";

<Route path="/levelfour" element={<LevelFourPage />} />

Verification

After registering both plugins:

curl http://localhost:7007/api/levelfour/health
curl http://localhost:7007/api/levelfour/recommendations/summary
curl http://localhost:7007/api/levelfour/costs/summary

Then open http://localhost:3000/levelfour to see the cost dashboard.

Reference

This plugin follows the same patterns as the Backstage Cost Insights plugin but uses the LevelFour API directly via the levelfour npm package.